/*
Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.

NVIDIA CORPORATION and its licensors retain all intellectual property
and proprietary rights in and to this software, related documentation
and any modifications thereto. Any use, reproduction, disclosure or
distribution of this software and related documentation without an express
license agreement from NVIDIA CORPORATION is strictly prohibited.
*/

#include "NRIFramework.h"
#include "Extensions/NRIRayTracing.h"

// NRD and NRI-based integration
#include "NRD.h"
#include "NRDIntegration.hpp"

// DLSS and NRI-based integration
#include "Extensions/NRIWrapperVK.h"
#include "DLSS/DLSSIntegration.hpp"

// NIS
#include "NGX/NVIDIAImageScaling/NIS/NIS_Config.h"

#ifdef _WIN32
    #undef APIENTRY
    #include <windows.h>
#endif

//=================================================================================
// Settings
//=================================================================================

// NRD mode and other shared settings are here
#include "../Shaders/Include/Shared.hlsli"

constexpr uint32_t MAX_ANIMATED_INSTANCE_NUM        = 512;
constexpr auto BLAS_RIGID_MESH_BUILD_BITS           = nri::AccelerationStructureBuildBits::PREFER_FAST_TRACE;
constexpr auto BLAS_DEFORMABLE_MESH_BUILD_BITS      = nri::AccelerationStructureBuildBits::PREFER_FAST_BUILD | nri::AccelerationStructureBuildBits::ALLOW_UPDATE;
constexpr auto TLAS_BUILD_BITS                      = nri::AccelerationStructureBuildBits::PREFER_FAST_TRACE;
constexpr float ACCUMULATION_TIME                   = 0.5f; // seconds
constexpr float NEAR_Z                              = 0.001f; // m
constexpr float GLASS_THICKNESS                     = 0.002f; // m
constexpr float CAMERA_BACKWARD_OFFSET              = 0.0f; // m, 3rd person camera offset
constexpr bool CAMERA_RELATIVE                      = true;
constexpr bool ALLOW_BLAS_MERGING                   = true;
constexpr bool NRD_ALLOW_DESCRIPTOR_CACHING         = true;
constexpr int32_t MAX_HISTORY_FRAME_NUM             = (int32_t)std::min(60u, std::min(nrd::REBLUR_MAX_HISTORY_FRAME_NUM, nrd::RELAX_MAX_HISTORY_FRAME_NUM));
constexpr uint32_t TEXTURES_PER_MATERIAL            = 4;
constexpr uint32_t MAX_TEXTURE_TRANSITIONS_NUM      = 32;
constexpr uint32_t DYNAMIC_CONSTANT_BUFFER_SIZE     = 1024 * 1024; // 1MB
constexpr uint32_t MAX_ANIMATION_HISTORY_FRAME_NUM  = 2;

//=================================================================================
// Important tests, sensitive to regressions or just testing base functionality
//=================================================================================

const std::vector<uint32_t> interior_checkMeTests =
{{
    1, 3, 6, 8, 9, 10, 12, 13, 14, 23, 27, 28, 29, 31, 32, 35, 43, 44, 47, 53,
    59, 60, 62, 67, 75, 76, 79, 81, 95, 96, 107, 109, 111, 110, 114, 120, 124,
    126, 127, 132, 133, 134, 139, 140, 142, 145, 148, 150, 155, 156, 157, 160,
    161, 162, 164, 168, 169, 171, 172, 173, 174
}};

//=================================================================================
// Tests, where IQ improvement would be "nice to have"
//=================================================================================

const std::vector<uint32_t> REBLUR_interior_improveMeTests =
{{
    108, 153, 174, 191, 192
}};

const std::vector<uint32_t> RELAX_interior_improveMeTests =
{{
    96, 114, 144, 148, 156, 159
}};

// TODO: add tests for SIGMA, active when "Shadow" visualization is on

//=================================================================================

#define _STRINGIFY(x) #x
#define STRINGIFY(x) _STRINGIFY(x)

// UI
#define UI_YELLOW                                   ImVec4(1.0f, 0.9f, 0.0f, 1.0f)
#define UI_GREEN                                    ImVec4(0.5f, 0.9f, 0.0f, 1.0f)
#define UI_RED                                      ImVec4(1.0f, 0.1f, 0.0f, 1.0f)
#define UI_HEADER                                   ImVec4(0.7f, 1.0f, 0.7f, 1.0f)
#define UI_HEADER_BACKGROUND                        ImVec4(0.7f * 0.3f, 1.0f * 0.3f, 0.7f * 0.3f, 1.0f)
#define UI_DEFAULT                                  ImGui::GetStyleColorVec4(ImGuiCol_Text)

enum MvType : int32_t
{
    MV_2D,
    MV_25D,
    MV_3D,
};

enum class AccelerationStructure : uint32_t
{
    TLAS_World,
    TLAS_Emissive,

    BLAS_StaticOpaque,
    BLAS_StaticTransparent,
    BLAS_StaticEmissive,

    BLAS_Other // all other BLAS start from here
};

enum class Buffer : uint32_t
{
    // DEVICE (read only)
    InstanceData,
    MorphMeshIndices,
    MorphMeshVertices,

    // DEVICE
    MorphedPositions,
    MorphedAttributes,
    MorphedPrimitivePrevData,
    PrimitiveData,
    WorldScratch,
    LightScratch,
    MorphMeshScratch,
};

enum class Texture : uint32_t
{
    Ambient,
    ViewZ,
    Mv,
    Normal_Roughness,
    PsrThroughput,
    BaseColor_Metalness,
    DirectLighting,
    DirectEmission,
    Shadow,
    Diff,
    Spec,
    Unfiltered_ShadowData,
    Unfiltered_Diff,
    Unfiltered_Spec,
    Unfiltered_Shadow_Translucency,
    Validation,
    Composed_ViewZ,
    DlssOutput,
    Final,

    // History
    ComposedDiff,
    ComposedSpec_ViewZ,
    TaaHistory,
    TaaHistoryPrev,

    // SH
#if( NRD_MODE == SH )
    Unfiltered_DiffSh,
    Unfiltered_SpecSh,
    DiffSh,
    SpecSh,
#endif

    // Read-only
    NisData1,
    NisData2,
    MaterialTextures,

    MAX_NUM,

    // Aliases
    DlssInput = Unfiltered_Diff
};

enum class Pipeline : uint32_t
{
    MorphMeshUpdateVertices,
    MorphMeshUpdatePrimitives,
    TraceAmbient,
    TraceOpaque,
    Composition,
    TraceTransparent,
    Temporal,
    Upsample,
    UpsampleNis,
    DlssBefore,
    DlssAfter,

    MAX_NUM,
};

enum class Descriptor : uint32_t
{
    World_AccelerationStructure,
    Light_AccelerationStructure,

    LinearMipmapLinear_Sampler,
    LinearMipmapNearest_Sampler,
    NearestMipmapNearest_Sampler,

    Global_ConstantBuffer,
    MorphTargetPose_ConstantBuffer,
    MorphTargetUpdatePrimitives_ConstantBuffer,

    InstanceData_Buffer,
    MorphMeshIndices_Buffer,
    MorphMeshVertices_Buffer,

    MorphedPositions_Buffer,
    MorphedPositions_StorageBuffer,
    MorphedAttributes_Buffer,
    MorphedAttributes_StorageBuffer,
    MorphedPrimitivePrevData_Buffer,
    MorphedPrimitivePrevData_StorageBuffer,
    PrimitiveData_Buffer,
    PrimitiveData_StorageBuffer,

    Ambient_Texture,
    Ambient_StorageTexture,
    ViewZ_Texture,
    ViewZ_StorageTexture,
    Mv_Texture,
    Mv_StorageTexture,
    Normal_Roughness_Texture,
    Normal_Roughness_StorageTexture,
    PsrThroughput_Texture,
    PsrThroughput_StorageTexture,
    BaseColor_Metalness_Texture,
    BaseColor_Metalness_StorageTexture,
    DirectLighting_Texture,
    DirectLighting_StorageTexture,
    DirectEmission_Texture,
    DirectEmission_StorageTexture,
    Shadow_Texture,
    Shadow_StorageTexture,
    Diff_Texture,
    Diff_StorageTexture,
    Spec_Texture,
    Spec_StorageTexture,
    Unfiltered_ShadowData_Texture,
    Unfiltered_ShadowData_StorageTexture,
    Unfiltered_Diff_Texture,
    Unfiltered_Diff_StorageTexture,
    Unfiltered_Spec_Texture,
    Unfiltered_Spec_StorageTexture,
    Unfiltered_Shadow_Translucency_Texture,
    Unfiltered_Shadow_Translucency_StorageTexture,
    Validation_Texture,
    Validation_StorageTexture,
    Composed_ViewZ_Texture,
    Composed_ViewZ_StorageTexture,
    DlssOutput_Texture,
    DlssOutput_StorageTexture,
    Final_Texture,
    Final_StorageTexture,

    // History
    ComposedDiff_Texture,
    ComposedDiff_StorageTexture,
    ComposedSpec_ViewZ_Texture,
    ComposedSpec_ViewZ_StorageTexture,
    TaaHistory_Texture,
    TaaHistory_StorageTexture,
    TaaHistoryPrev_Texture,
    TaaHistoryPrev_StorageTexture,

    // SH
#if( NRD_MODE == SH )
    Unfiltered_DiffSh_Texture,
    Unfiltered_DiffSh_StorageTexture,
    Unfiltered_SpecSh_Texture,
    Unfiltered_SpecSh_StorageTexture,
    DiffSh_Texture,
    DiffSh_StorageTexture,
    SpecSh_Texture,
    SpecSh_StorageTexture,
#endif

    // Read-only
    NisData1,
    NisData2,
    MaterialTextures,

    MAX_NUM,

    // Aliases
    DlssInput_Texture = Unfiltered_Diff_Texture,
    DlssInput_StorageTexture = Unfiltered_Diff_StorageTexture
};

enum class DescriptorSet : uint32_t
{
    Global0,
    TraceAmbient1,
    TraceOpaque1,
    Composition1,
    TraceTransparent1,
    Temporal1a,
    Temporal1b,
    Upsample1,
    Upsample1a,
    Upsample1b,
    UpsampleNis1,
    UpsampleNis1a,
    UpsampleNis1b,
    DlssBefore1,
    DlssAfter1,
    RayTracing2,
    MorphTargetPose3,
    MorphTargetUpdatePrimitives3,

    MAX_NUM
};

// NRD sample doesn't use several instances of the same denoiser in one NRD instance (like REBLUR_DIFFUSE x 3),
// thus we can use fields of "nrd::Denoiser" enum as unique identifiers
#define NRD_ID(x) nrd::Identifier(nrd::Denoiser::x)

struct NRIInterface
    : public nri::CoreInterface
    , public nri::HelperInterface
    , public nri::StreamerInterface
    , public nri::SwapChainInterface
    , public nri::RayTracingInterface
{};

struct Frame
{
    nri::CommandAllocator* commandAllocator;
    nri::CommandBuffer* commandBuffer;
};

struct Settings
{
    double      motionStartTime                    = 0.0;

    float       maxFps                             = 60.0f;
    float       camFov                             = 90.0f;
    float       sunAzimuth                         = -147.0f;
    float       sunElevation                       = 45.0f;
    float       sunAngularDiameter                 = 0.533f;
    float       exposure                           = 80.0f;
    float       roughnessOverride                  = 0.0f;
    float       metalnessOverride                  = 0.0f;
    float       emissionIntensity                  = 1.0f;
    float       debug                              = 0.0f;
    float       meterToUnitsMultiplier             = 1.0f;
    float       emulateMotionSpeed                 = 1.0f;
    float       animatedObjectScale                = 1.0f;
    float       separator                          = 0.0f;
    float       animationProgress                  = 0.0f;
    float       animationSpeed                     = 0.0f;
    float       hitDistScale                       = 3.0f;
    float       disocclusionThreshold              = 1.0f;
    float       resolutionScale                    = 1.0f;
    float       sharpness                          = 0.15f;

    int32_t     maxAccumulatedFrameNum             = 31;
    int32_t     maxFastAccumulatedFrameNum         = 7;
    int32_t     onScreen                           = 0;
    int32_t     forcedMaterial                     = 0;
    int32_t     animatedObjectNum                  = 5;
    int32_t     activeAnimation                    = 0;
    int32_t     motionMode                         = 0;
    int32_t     denoiser                           = DENOISER_REBLUR;
    int32_t     rpp                                = 1;
    int32_t     bounceNum                          = 1;
    int32_t     tracingMode                        = RESOLUTION_HALF;
    int32_t     mvType                             = MV_25D;

    bool        cameraJitter                       = true;
    bool        limitFps                           = false;
    bool        ambient                            = true;
    bool        PSR                                = false;
    bool        indirectDiffuse                    = true;
    bool        indirectSpecular                   = true;
    bool        normalMap                          = true;
    bool        TAA                                = true;
    bool        animatedObjects                    = false;
    bool        animateScene                       = false;
    bool        animateSun                         = false;
    bool        nineBrothers                       = false;
    bool        blink                              = false;
    bool        pauseAnimation                     = true;
    bool        emission                           = false;
    bool        linearMotion                       = true;
    bool        emissiveObjects                    = false;
    bool        importanceSampling                 = true;
    bool        specularLobeTrimming               = true;
    bool        ortho                              = false;
    bool        adaptiveAccumulation               = true;
    bool        usePrevFrame                       = true;
    bool        windowAlignment                    = true;
    bool        NIS                                = true;
    bool        SR                                 = false;
    bool        RR                                 = false;
};

struct DescriptorDesc
{
    const char* debugName;
    void* resource;
    nri::Format format;
    nri::TextureUsageBits textureUsage;
    nri::BufferUsageBits bufferUsage;
    bool isArray;
};

struct TextureState
{
    Texture texture;
    nri::AccessLayoutStage after;
};

struct AnimatedInstance
{
    float3 basePosition;
    float3 rotationAxis;
    float3 elipseAxis;
    float durationSec = 5.0f;
    float progressedSec = 0.0f;
    uint32_t instanceID = 0;
    bool reverseRotation = true;
    bool reverseDirection = true;

    float4x4 Animate(float elapsedSeconds, float scale, float3& position)
    {
        float angle = progressedSec / durationSec;
        angle = Pi(angle * 2.0f - 1.0f);

        float3 localPosition;
        localPosition.x = Cos(reverseDirection ? -angle : angle);
        localPosition.y = Sin(reverseDirection ? -angle : angle);
        localPosition.z = localPosition.y;

        position = basePosition + localPosition * elipseAxis * scale;

        float4x4 transform;
        transform.SetupByRotation(reverseRotation ? -angle : angle, rotationAxis);
        transform.AddScale(scale);

        progressedSec = Mod(progressedSec + elapsedSeconds, durationSec);

        return transform;
    }
};

class Sample : public SampleBase
{
public:
    Sample() :
        m_NRD(BUFFERED_FRAME_MAX_NUM, NRD_ALLOW_DESCRIPTOR_CACHING, "NRD")
    {}

    ~Sample();

    inline float GetDenoisingRange() const
    { return 4.0f * m_Scene.aabb.GetRadius(); }

    inline bool IsNisAllowed() const
    { return m_Settings.separator == 0.0f && !m_ShowValidationOverlay; }

    inline nrd::RelaxSettings GetDefaultRelaxSettings() const
    { return {}; }

    inline bool IsDlssEnabled() const
    { return m_Settings.SR || m_Settings.RR; }

    inline nri::Texture*& Get(Texture index)
    { return m_Textures[(uint32_t)index]; }

    inline nri::TextureBarrierDesc& GetState(Texture index)
    { return m_TextureStates[(uint32_t)index]; }

    inline nri::Format GetFormat(Texture index)
    { return m_TextureFormats[(uint32_t)index]; }

    inline nri::Buffer*& Get(Buffer index)
    { return m_Buffers[(uint32_t)index]; }

    inline nri::Pipeline*& Get(Pipeline index)
    { return m_Pipelines[(uint32_t)index]; }

    inline nri::Descriptor*& Get(Descriptor index)
    { return m_Descriptors[(uint32_t)index]; }

    inline nri::DescriptorSet*& Get(DescriptorSet index)
    { return m_DescriptorSets[(uint32_t)index]; }

    inline nri::AccelerationStructure*& Get(AccelerationStructure index)
    { return m_AccelerationStructures[(uint32_t)index]; }

    inline void InitCmdLine(cmdline::parser& cmdLine) override
    {
        cmdLine.add<int32_t>("dlssQuality", 'd', "DLSS quality: [-1: 4]", false, -1, cmdline::range(-1, 4));
        cmdLine.add("debugNRD", 0, "enable NRD validation");
    }

    inline void ReadCmdLine(cmdline::parser& cmdLine) override
    {
        m_DlssQuality = cmdLine.get<int32_t>("dlssQuality");
        m_DebugNRD = cmdLine.exist("debugNRD");
    }

    inline nrd::ReblurSettings GetDefaultReblurSettings() const
    {
        nrd::ReblurSettings defaults = {};
        defaults.antilagSettings.luminanceAntilagPower = 1.0f;

        return defaults;
    }

    inline float3 GetSunDirection() const
    {
        float3 sunDirection;
        sunDirection.x = Cos( DegToRad(m_Settings.sunAzimuth) ) * Cos( DegToRad(m_Settings.sunElevation) );
        sunDirection.y = Sin( DegToRad(m_Settings.sunAzimuth) ) * Cos( DegToRad(m_Settings.sunElevation) );
        sunDirection.z = Sin( DegToRad(m_Settings.sunElevation) );

        return sunDirection;
    }

    bool Initialize(nri::GraphicsAPI graphicsAPI) override;
    void LatencySleep(uint32_t frameIndex) override;
    void PrepareFrame(uint32_t frameIndex) override;
    void RenderFrame(uint32_t frameIndex) override;

    void LoadScene();
    void AddInnerGlassSurfaces();
    void GenerateAnimatedCubes();
    nri::Format CreateSwapChain();
    void CreateCommandBuffers();
    void CreatePipelineLayoutAndDescriptorPool();
    void CreatePipelines();
    void CreateAccelerationStructures();
    void CreateSamplers();
    void CreateResources(nri::Format swapChainFormat);
    void CreateDescriptorSets();
    void CreateTexture(std::vector<DescriptorDesc>& descriptorDescs, const char* debugName, nri::Format format, nri::Dim_t width, nri::Dim_t height, nri::Mip_t mipNum, nri::Dim_t arraySize, nri::TextureUsageBits usage, nri::AccessBits state);
    void CreateBuffer(std::vector<DescriptorDesc>& descriptorDescs, const char* debugName, uint64_t elements, uint32_t stride, nri::BufferUsageBits usage, nri::Format format = nri::Format::UNKNOWN);
    void UploadStaticData();
    void UpdateConstantBuffer(uint32_t frameIndex, uint32_t maxAccumulatedFrameNum);
    void RestoreBindings(nri::CommandBuffer& commandBuffer);
    void GatherInstanceData();
    uint16_t BuildOptimizedTransitions(const TextureState* states, uint32_t stateNum, std::array<nri::TextureBarrierDesc, MAX_TEXTURE_TRANSITIONS_NUM>& transitions);

private:
    // NRD
    NrdIntegration m_NRD;
    nrd::CommonSettings m_CommonSettings = {};
    nrd::RelaxSettings m_RelaxSettings = {};
    nrd::ReblurSettings m_ReblurSettings = {};
    nrd::ReferenceSettings m_ReferenceSettings = {};

    // DLSS
    DlssIntegration m_DLSS;

    // NRI
    NRIInterface NRI = {};
    utils::Scene m_Scene;
    nri::Device* m_Device = nullptr;
    nri::Streamer* m_Streamer = nullptr;
    nri::SwapChain* m_SwapChain = nullptr;
    nri::CommandQueue* m_CommandQueue = nullptr;
    nri::Fence* m_FrameFence;
    nri::DescriptorPool* m_DescriptorPool = nullptr;
    nri::PipelineLayout* m_PipelineLayout = nullptr;
    std::array<Frame, BUFFERED_FRAME_MAX_NUM> m_Frames = {};
    std::vector<nri::Texture*> m_Textures;
    std::vector<nri::TextureBarrierDesc> m_TextureStates;
    std::vector<nri::Format> m_TextureFormats;
    std::vector<nri::Buffer*> m_Buffers;
    std::vector<nri::Memory*> m_MemoryAllocations;
    std::vector<nri::Descriptor*> m_Descriptors;
    std::vector<nri::DescriptorSet*> m_DescriptorSets;
    std::vector<nri::Pipeline*> m_Pipelines;
    std::vector<nri::AccelerationStructure*> m_AccelerationStructures;
    std::vector<BackBuffer> m_SwapChainBuffers;

    // Data
    std::vector<InstanceData> m_InstanceData;
    std::vector<nri::GeometryObjectInstance> m_WorldTlasData;
    std::vector<nri::GeometryObjectInstance> m_LightTlasData;
    std::vector<AnimatedInstance> m_AnimatedInstances;
    std::array<float, 256> m_FrameTimes = {};
    Settings m_Settings = {};
    Settings m_SettingsPrev = {};
    Settings m_SettingsDefault = {};
    const std::vector<uint32_t>* m_checkMeTests = nullptr;
    const std::vector<uint32_t>* m_improveMeTests = nullptr;
    float4 m_HairBaseColorOverride = float4(0.227f, 0.130f, 0.035f, 1.0f);
    float3 m_PrevLocalPos = {};
    float2 m_HairBetasOverride = float2(0.25f, 0.6f);
    uint2 m_RenderResolution = {};
    uint64_t m_MorphMeshScratchSize = 0;
    uint64_t m_WorldTlasDataOffsetInDynamicBuffer = 0;
    uint64_t m_LightTlasDataOffsetInDynamicBuffer = 0;
    uint32_t m_GlobalConstantBufferOffset = 0;
    uint32_t m_OpaqueObjectsNum = 0;
    uint32_t m_TransparentObjectsNum = 0;
    uint32_t m_EmissiveObjectsNum = 0;
    uint32_t m_ProxyInstancesNum = 0;
    uint32_t m_LastSelectedTest = uint32_t(-1);
    uint32_t m_TestNum = uint32_t(-1);
    int32_t m_DlssQuality = int32_t(-1);
    float m_UiWidth = 0.0f;
    float m_MinResolutionScale = 0.5f;
    float m_DofAperture = 0.0f;
    float m_DofFocalDistance = 1.0f;
    float m_SdrScale = 1.0f;
    bool m_HasTransparent = false;
    bool m_ShowUi = true;
    bool m_ForceHistoryReset = false;
    bool m_Resolve = true;
    bool m_DebugNRD = false;
    bool m_ShowValidationOverlay = false;
    bool m_PositiveZ = true;
    bool m_ReversedZ = false;
    bool m_IsSrgb = false;
};

Sample::~Sample()
{
    if (!m_Device)
        return;

    NRI.WaitForIdle(*m_CommandQueue);

    m_DLSS.Shutdown();

    m_NRD.Destroy();

    for (Frame& frame : m_Frames)
    {
        NRI.DestroyCommandBuffer(*frame.commandBuffer);
        NRI.DestroyCommandAllocator(*frame.commandAllocator);
    }

    for (BackBuffer& backBuffer : m_SwapChainBuffers)
        NRI.DestroyDescriptor(*backBuffer.colorAttachment);

    for (uint32_t i = 0; i < m_Textures.size(); i++)
        NRI.DestroyTexture(*m_Textures[i]);

    for (uint32_t i = 0; i < m_Buffers.size(); i++)
        NRI.DestroyBuffer(*m_Buffers[i]);

    for (uint32_t i = 0; i < m_Descriptors.size(); i++)
        NRI.DestroyDescriptor(*m_Descriptors[i]);

    for (uint32_t i = 0; i < m_Pipelines.size(); i++)
        NRI.DestroyPipeline(*m_Pipelines[i]);

    for (uint32_t i = 0; i < m_AccelerationStructures.size(); i++)
    {
        if (m_AccelerationStructures[i])
            NRI.DestroyAccelerationStructure(*m_AccelerationStructures[i]);
    }

    NRI.DestroyPipelineLayout(*m_PipelineLayout);
    NRI.DestroyDescriptorPool(*m_DescriptorPool);
    NRI.DestroyFence(*m_FrameFence);
    NRI.DestroySwapChain(*m_SwapChain);
    NRI.DestroyStreamer(*m_Streamer);

    for (size_t i = 0; i < m_MemoryAllocations.size(); i++)
    {
        if (m_MemoryAllocations[i])
            NRI.FreeMemory(*m_MemoryAllocations[i]);
    }

    DestroyUI(NRI);

    nri::nriDestroyDevice(*m_Device);
}

bool Sample::Initialize(nri::GraphicsAPI graphicsAPI)
{
    Rand::Seed(106937, &m_FastRandState);

    nri::AdapterDesc bestAdapterDesc = {};
    uint32_t adapterDescsNum = 1;
    NRI_ABORT_ON_FAILURE(nri::nriEnumerateAdapters(&bestAdapterDesc, adapterDescsNum));

    nri::DeviceCreationDesc deviceCreationDesc = {};
    deviceCreationDesc.graphicsAPI = graphicsAPI;
    deviceCreationDesc.enableAPIValidation = m_DebugAPI;
    deviceCreationDesc.enableNRIValidation = m_DebugNRI;
    deviceCreationDesc.spirvBindingOffsets = SPIRV_BINDING_OFFSETS;
    deviceCreationDesc.adapterDesc = &bestAdapterDesc;
    if (bestAdapterDesc.vendor == nri::Vendor::NVIDIA)
        DlssIntegration::SetupDeviceExtensions(deviceCreationDesc);

    NRI_ABORT_ON_FAILURE( nri::nriCreateDevice(deviceCreationDesc, m_Device) );

    NRI_ABORT_ON_FAILURE( nri::nriGetInterface(*m_Device, NRI_INTERFACE(nri::CoreInterface), (nri::CoreInterface*)&NRI) );
    NRI_ABORT_ON_FAILURE( nri::nriGetInterface(*m_Device, NRI_INTERFACE(nri::HelperInterface), (nri::HelperInterface*)&NRI) );
    NRI_ABORT_ON_FAILURE( nri::nriGetInterface(*m_Device, NRI_INTERFACE(nri::StreamerInterface), (nri::StreamerInterface*)&NRI) );
    NRI_ABORT_ON_FAILURE( nri::nriGetInterface(*m_Device, NRI_INTERFACE(nri::SwapChainInterface), (nri::SwapChainInterface*)&NRI) );
    NRI_ABORT_ON_FAILURE( nri::nriGetInterface(*m_Device, NRI_INTERFACE(nri::RayTracingInterface), (nri::RayTracingInterface*)&NRI) );

    NRI_ABORT_ON_FAILURE( NRI.GetCommandQueue(*m_Device, nri::CommandQueueType::GRAPHICS, m_CommandQueue) );
    NRI_ABORT_ON_FAILURE( NRI.CreateFence(*m_Device, 0, m_FrameFence) );

    // Create streamer
    nri::StreamerDesc streamerDesc = {};
    streamerDesc.constantBufferMemoryLocation = nri::MemoryLocation::HOST_UPLOAD;
    streamerDesc.constantBufferSize = DYNAMIC_CONSTANT_BUFFER_SIZE;
    streamerDesc.dynamicBufferMemoryLocation = nri::MemoryLocation::HOST_UPLOAD;
    streamerDesc.dynamicBufferUsageBits = nri::BufferUsageBits::VERTEX_BUFFER | nri::BufferUsageBits::INDEX_BUFFER | nri::BufferUsageBits::ACCELERATION_STRUCTURE_BUILD_READ;
    streamerDesc.frameInFlightNum = BUFFERED_FRAME_MAX_NUM + 1; // TODO: "+1" for just in case?
    NRI_ABORT_ON_FAILURE( NRI.CreateStreamer(*m_Device, streamerDesc, m_Streamer) );

    // Initialize DLSS
    m_RenderResolution = GetOutputResolution();

    if (m_DlssQuality != -1 && m_DLSS.InitializeLibrary(*m_Device, ""))
    {
        DlssInitDesc dlssInitDesc = {};
        dlssInitDesc.outputResolution = {GetOutputResolution().x, GetOutputResolution().y};
        dlssInitDesc.quality = (DlssQuality)m_DlssQuality;
        dlssInitDesc.hasHdrContent = NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION;

        DlssSettings dlssSettings = {};
        bool result = m_DLSS.GetOptimalSettings(dlssInitDesc.outputResolution, (DlssQuality)m_DlssQuality, dlssSettings);
        if (result)
        {
            float sx = float(dlssSettings.dynamicResolutionMin.Width) / float(dlssSettings.optimalResolution.Width);
            float sy = float(dlssSettings.dynamicResolutionMin.Height) / float(dlssSettings.optimalResolution.Height);

            m_RenderResolution = {dlssSettings.optimalResolution.Width, dlssSettings.optimalResolution.Height};
            m_MinResolutionScale = sy > sx ? sy : sx;

            printf("Render resolution (%u, %u)\n", m_RenderResolution.x, m_RenderResolution.y);

            result = m_DLSS.Initialize(m_CommandQueue, dlssInitDesc);
        }

        if (!result)
        {
            printf("DLSS: initialization failed!\n");
            m_DLSS.Shutdown();
        }

        m_Settings.SR = m_DLSS.HasSR();
        m_Settings.RR = m_DLSS.HasRR();
    }

    // Initialize NRD: REBLUR, RELAX and SIGMA in one instance
    {
        const nrd::DenoiserDesc denoisersDescs[] =
        {
            // REBLUR
    #if( NRD_MODE == OCCLUSION )
        #if( NRD_COMBINED == 1 )
            { NRD_ID(REBLUR_DIFFUSE_SPECULAR_OCCLUSION), nrd::Denoiser::REBLUR_DIFFUSE_SPECULAR_OCCLUSION },
        #else
            { NRD_ID(REBLUR_DIFFUSE_OCCLUSION), nrd::Denoiser::REBLUR_DIFFUSE_OCCLUSION },
            { NRD_ID(REBLUR_SPECULAR_OCCLUSION), nrd::Denoiser::REBLUR_SPECULAR_OCCLUSION },
        #endif
    #elif( NRD_MODE == SH )
        #if( NRD_COMBINED == 1 )
            { NRD_ID(REBLUR_DIFFUSE_SPECULAR_SH), nrd::Denoiser::REBLUR_DIFFUSE_SPECULAR_SH },
        #else
            { NRD_ID(REBLUR_DIFFUSE_SH), nrd::Denoiser::REBLUR_DIFFUSE_SH },
            { NRD_ID(REBLUR_SPECULAR_SH), nrd::Denoiser::REBLUR_SPECULAR_SH },
        #endif
    #elif( NRD_MODE == DIRECTIONAL_OCCLUSION )
            { NRD_ID(REBLUR_DIFFUSE_DIRECTIONAL_OCCLUSION), nrd::Denoiser::REBLUR_DIFFUSE_DIRECTIONAL_OCCLUSION },
    #else
        #if( NRD_COMBINED == 1 )
            { NRD_ID(REBLUR_DIFFUSE_SPECULAR), nrd::Denoiser::REBLUR_DIFFUSE_SPECULAR },
        #else
            { NRD_ID(REBLUR_DIFFUSE), nrd::Denoiser::REBLUR_DIFFUSE },
            { NRD_ID(REBLUR_SPECULAR), nrd::Denoiser::REBLUR_SPECULAR },
        #endif
    #endif

            // RELAX
    #if( NRD_MODE == SH )
        #if( NRD_COMBINED == 1 )
            { NRD_ID(RELAX_DIFFUSE_SPECULAR_SH), nrd::Denoiser::RELAX_DIFFUSE_SPECULAR_SH },
        #else
            { NRD_ID(RELAX_DIFFUSE_SH), nrd::Denoiser::RELAX_DIFFUSE_SH },
            { NRD_ID(RELAX_SPECULAR_SH), nrd::Denoiser::RELAX_SPECULAR_SH },
        #endif
    #else
        #if( NRD_COMBINED == 1 )
            { NRD_ID(RELAX_DIFFUSE_SPECULAR), nrd::Denoiser::RELAX_DIFFUSE_SPECULAR },
        #else
            { NRD_ID(RELAX_DIFFUSE), nrd::Denoiser::RELAX_DIFFUSE },
            { NRD_ID(RELAX_SPECULAR), nrd::Denoiser::RELAX_SPECULAR },
        #endif
    #endif

            // SIGMA
    #if( NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION )
            { NRD_ID(SIGMA_SHADOW_TRANSLUCENCY), nrd::Denoiser::SIGMA_SHADOW_TRANSLUCENCY },
    #endif

            // REFERENCE
            { NRD_ID(REFERENCE), nrd::Denoiser::REFERENCE },
        };

        nrd::InstanceCreationDesc instanceCreationDesc = {};
        instanceCreationDesc.denoisers = denoisersDescs;
        instanceCreationDesc.denoisersNum = helper::GetCountOf(denoisersDescs);

        nri::VideoMemoryInfo videoMemoryInfo1 = {};
        nri::nriQueryVideoMemoryInfo(*m_Device, nri::MemoryLocation::DEVICE, videoMemoryInfo1);

        NRI_ABORT_ON_FALSE( m_NRD.Initialize((uint16_t)m_RenderResolution.x, (uint16_t)m_RenderResolution.y, instanceCreationDesc, *m_Device, NRI, NRI) );

        nri::VideoMemoryInfo videoMemoryInfo2 = {};
        nri::nriQueryVideoMemoryInfo(*m_Device, nri::MemoryLocation::DEVICE, videoMemoryInfo2);
        printf("NRD: allocated %.2f Mb for REBLUR, RELAX, SIGMA and REFERENCE denoisers\n", (videoMemoryInfo2.currentUsage - videoMemoryInfo1.currentUsage) / (1024.0f * 1024.0f));
    }

    #if 0
        // README "Memory requirements" table generator
        printf("| %10s | %36s | %16s | %16s | %16s |\n", "Resolution", "Denoiser", "Working set (Mb)", "Persistent (Mb)", "Aliasable (Mb)");
        printf("|------------|--------------------------------------|------------------|------------------|------------------|\n");

        for (uint32_t j = 0; j < 3; j++)
        {
            const char* resolution = "1080p";
            uint16_t w = 1920;
            uint16_t h = 1080;

            if (j == 1)
            {
                resolution = "1440p";
                w = 2560;
                h = 1440;
            }
            else if (j == 2)
            {
                resolution = "2160p";
                w = 3840;
                h = 2160;
            }

            for (uint32_t i = 0; i <= (uint32_t)nrd::Denoiser::REFERENCE; i++)
            {
                nrd::Denoiser denoiser = (nrd::Denoiser)i;
                const char* methodName = nrd::GetDenoiserString(denoiser);

                const nrd::DenoiserDesc denoiserDesc = {0, denoiser, w, h};

                nrd::InstanceCreationDesc instanceCreationDesc = {};
                instanceCreationDesc.denoisers = &denoiserDesc;
                instanceCreationDesc.denoisersNum = 1;

                NrdIntegration instance(2);
                NRI_ABORT_ON_FALSE( instance.Initialize(instanceCreationDesc, *m_Device, NRI, NRI) );
                printf("| %10s | %36s | %16.2f | %16.2f | %16.2f |\n", i == 0 ? resolution : "", methodName, instance.GetTotalMemoryUsageInMb(), instance.GetPersistentMemoryUsageInMb(), instance.GetAliasableMemoryUsageInMb());
                instance.Destroy();
            }

            if (j != 2)
                printf("| %10s | %36s | %16s | %16s | %16s |\n", "", "", "", "", "");
        }

        __debugbreak();
    #endif

    LoadScene();

    if (m_SceneFile.find("BistroInterior") != std::string::npos)
        AddInnerGlassSurfaces();

    GenerateAnimatedCubes();

    nri::Format swapChainFormat = CreateSwapChain();
    CreateCommandBuffers();
    CreatePipelineLayoutAndDescriptorPool();
    CreatePipelines();
    CreateAccelerationStructures();
    CreateSamplers();
    CreateResources(swapChainFormat);
    CreateDescriptorSets();

    UploadStaticData();

    m_Camera.Initialize(m_Scene.aabb.GetCenter(), m_Scene.aabb.vMin, CAMERA_RELATIVE);
    m_Scene.UnloadTextureData();
    m_Scene.UnloadGeometryData();

    m_SettingsDefault = m_Settings;
    m_ShowValidationOverlay = m_DebugNRD;

    return InitUI(NRI, NRI, *m_Device, swapChainFormat);
}

void Sample::LatencySleep(uint32_t frameIndex)
{
    const Frame& frame = m_Frames[frameIndex % BUFFERED_FRAME_MAX_NUM];
    if (frameIndex >= BUFFERED_FRAME_MAX_NUM)
    {
        NRI.Wait(*m_FrameFence, 1 + frameIndex - BUFFERED_FRAME_MAX_NUM);
        NRI.ResetCommandAllocator(*frame.commandAllocator);
    }
}

void Sample::PrepareFrame(uint32_t frameIndex)
{
    m_ForceHistoryReset = false;
    m_SettingsPrev = m_Settings;
    m_Camera.SavePreviousState();

    if (IsKeyToggled(Key::Tab))
        m_ShowUi = !m_ShowUi;
    if (IsKeyToggled(Key::F1))
        m_Settings.debug = Step(0.5f, 1.0f - m_Settings.debug);
    if (IsKeyToggled(Key::F3))
        m_Settings.emission = !m_Settings.emission;
    if (IsKeyToggled(Key::Space))
        m_Settings.pauseAnimation = !m_Settings.pauseAnimation;
    if (IsKeyToggled(Key::PageDown) || IsKeyToggled(Key::Num3))
    {
        m_Settings.denoiser++;
        if (m_Settings.denoiser > DENOISER_REFERENCE)
            m_Settings.denoiser = DENOISER_REBLUR;
    }
    if (IsKeyToggled(Key::PageUp) || IsKeyToggled(Key::Num9))
    {
        m_Settings.denoiser--;
        if (m_Settings.denoiser < DENOISER_REBLUR)
            m_Settings.denoiser = DENOISER_REFERENCE;
    }

    BeginUI();
    if (!IsKeyPressed(Key::LAlt) && m_ShowUi)
    {
        ImGui::SetNextWindowPos(ImVec2(m_Settings.windowAlignment ? 5.0f : GetOutputResolution().x - m_UiWidth - 5.0f, 5.0f));
        ImGui::SetNextWindowSize(ImVec2(0.0f, 0.0f));
        ImGui::Begin("Settings [Tab]", nullptr, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize);
        {
            float avgFrameTime = m_Timer.GetVerySmoothedFrameTime();

            char buf[256];
            snprintf(buf, sizeof(buf), "%.1f FPS (%.2f ms)", 1000.0f / avgFrameTime, avgFrameTime);

            ImVec4 colorFps = UI_GREEN;
            if (avgFrameTime > 1000.0f / 59.5f)
                colorFps = UI_YELLOW;
            if (avgFrameTime > 1000.0f / 29.5f)
                colorFps = UI_RED;

            float lo = avgFrameTime * 0.5f;
            float hi = avgFrameTime * 1.5f;

            const uint32_t N = helper::GetCountOf(m_FrameTimes);
            uint32_t head = frameIndex % N;
            m_FrameTimes[head] = m_Timer.GetFrameTime();
            ImGui::PushStyleColor(ImGuiCol_Text, colorFps);
                ImGui::PlotLines("", m_FrameTimes.data(), N, head, buf, lo, hi, ImVec2(0.0f, 70.0f));
            ImGui::PopStyleColor();

            if (IsButtonPressed(Button::Right))
            {
                ImGui::Text("Move - W/S/A/D");
                ImGui::Text("Accelerate - MOUSE SCROLL");
            }
            else
            {
                // "Camera" section
                ImGui::NewLine();
                ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                bool isUnfolded = ImGui::CollapsingHeader("CAMERA (press RIGHT MOUSE BOTTON for free-fly mode)", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                ImGui::PopStyleColor();
                ImGui::PopStyleColor();

                ImGui::PushID("CAMERA");
                if (isUnfolded)
                {
                    static const char* onScreenModes[] =
                    {
                #if( NRD_MODE == OCCLUSION )
                        "Diffuse occlusion",
                        "Specular occlusion",
                #elif( NRD_MODE == DIRECTIONAL_OCCLUSION )
                        "Diffuse occlusion",
                #else
                        "Final",
                        "Denoised diffuse",
                        "Denoised specular",
                        "Diffuse occlusion",
                        "Specular occlusion",
                        "Shadow",
                        "Base color",
                        "Normal",
                        "Roughness",
                        "Metalness",
                        "Material ID",
                        "PSR throughput",
                        "World units",
                        "Instance index",
                        "UV",
                        "Curvature",
                        "Mip level (primary)",
                        "Mip level (specular)",
                #endif
                    };

                    static const char* motionMode[] =
                    {
                        "Left / Right",
                        "Up / Down",
                        "Forward / Backward",
                        "Mixed",
                        "Pan",
                    };

                    static const char* mvType[] =
                    {
                        "2D",
                        "2.5D",
                        "3D",
                    };

                    ImGui::Combo("On screen", &m_Settings.onScreen, onScreenModes, helper::GetCountOf(onScreenModes));
                    ImGui::Checkbox("Ortho", &m_Settings.ortho);
                    ImGui::SameLine();
                    ImGui::Checkbox("+Z", &m_PositiveZ);
                    ImGui::SameLine();
                    ImGui::Checkbox("rZ", &m_ReversedZ);
                    ImGui::SameLine();
                    ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                    ImGui::SliderFloat("FOV (deg)", &m_Settings.camFov, 1.0f, 160.0f, "%.1f");
                    ImGui::SliderFloat("Exposure", &m_Settings.exposure, 0.0f, 1000.0f, "%.3f", ImGuiSliderFlags_Logarithmic);
                    ImGui::SliderFloat("Resolution scale (%)", &m_Settings.resolutionScale, m_MinResolutionScale, 1.0f, "%.3f");
                    ImGui::SliderFloat("Aperture (mm)", &m_DofAperture, 0.0f, 100.0f, "%.2f");
                    ImGui::SliderFloat("Focal distance (m)", &m_DofFocalDistance, NEAR_Z, 10.0f, "%.3f");
                    ImGui::Checkbox("FPS cap", &m_Settings.limitFps);
                    ImGui::SameLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, (!m_Settings.cameraJitter && (m_Settings.TAA || IsDlssEnabled())) ? UI_RED : UI_DEFAULT);
                        ImGui::Checkbox("Jitter", &m_Settings.cameraJitter);
                    ImGui::PopStyleColor();
                    ImGui::SameLine();
                    ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                    ImGui::PushStyleColor(ImGuiCol_Text, (m_Settings.animatedObjects && !m_Settings.pauseAnimation && m_Settings.mvType == MV_2D) ? UI_RED : UI_DEFAULT);
                        ImGui::Combo("MV", &m_Settings.mvType, mvType, helper::GetCountOf(mvType));
                    ImGui::PopStyleColor();

                    ImGui::PushStyleColor(ImGuiCol_Text, m_Settings.motionStartTime > 0.0 ? UI_YELLOW : UI_DEFAULT);
                        bool isPressed = ImGui::Button("Animation");
                    ImGui::PopStyleColor();
                    if (isPressed)
                        m_Settings.motionStartTime = m_Settings.motionStartTime > 0.0 ? 0.0 : -1.0;
                    if (m_Settings.motionStartTime > 0.0)
                    {
                        ImGui::SameLine();
                        ImGui::Checkbox("Linear", &m_Settings.linearMotion);
                        ImGui::SameLine();
                        ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                        ImGui::Combo("Mode", &m_Settings.motionMode, motionMode, helper::GetCountOf(motionMode));
                        ImGui::SliderFloat("Slower / Faster", &m_Settings.emulateMotionSpeed, -10.0f, 10.0f);
                    }

                    if (m_Settings.limitFps)
                        ImGui::SliderFloat("Min / Max FPS", &m_Settings.maxFps, 30.0f, 120.0f, "%.0f");
                }
                ImGui::PopID();

                // "Antialiasing, upscaling & sharpening" section
                ImGui::NewLine();
                ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                isUnfolded = ImGui::CollapsingHeader("ANTIALIASING & UPSCALING", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                ImGui::PopStyleColor();
                ImGui::PopStyleColor();

                ImGui::PushID("ANTIALIASING & UPSCALING");
                if (isUnfolded)
                {
                    if (m_DLSS.HasRR())
                    {
                        ImGui::Checkbox("DLSS-RR", &m_Settings.RR);
                        ImGui::SameLine();
                    }
                    if (m_DLSS.HasSR() && !m_Settings.RR)
                    {
                        ImGui::Checkbox("DLSS-SR", &m_Settings.SR);
                        ImGui::SameLine();
                    }
                    if (!m_Settings.SR)
                    {
                        ImGui::Checkbox("TAA", &m_Settings.TAA);
                        ImGui::SameLine();
                    }

                    if (IsNisAllowed())
                    {
                        ImGui::Checkbox("NIS", &m_Settings.NIS);
                        ImGui::SameLine();

                        if (m_Settings.NIS)
                        {
                            ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                            ImGui::SliderFloat("Sharpness", &m_Settings.sharpness, 0.0f, 1.0f, "%.2f");
                        }
                    }
                }
                ImGui::PopID();

                // "Materials" section
                ImGui::NewLine();
                ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                isUnfolded = ImGui::CollapsingHeader("MATERIALS", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                ImGui::PopStyleColor();
                ImGui::PopStyleColor();

                ImGui::PushID("MATERIALS");
                if (isUnfolded)
                {
                    static const char* forcedMaterial[] =
                    {
                        "None",
                        "Gypsum",
                        "Cobalt",
                    };

                    ImGui::SliderFloat2("Roughness / Metalness", &m_Settings.roughnessOverride, 0.0f, 1.0f, "%.3f");
                    ImGui::PushStyleColor(ImGuiCol_Text, (m_Settings.emissiveObjects && !m_Settings.emission) ? UI_YELLOW : UI_DEFAULT);
                        ImGui::Checkbox("Emission [F3]", &m_Settings.emission);
                    ImGui::PopStyleColor();
                    ImGui::SameLine();
                    ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                    ImGui::Combo("Material", &m_Settings.forcedMaterial, forcedMaterial, helper::GetCountOf(forcedMaterial));
                    if (m_Settings.emission)
                        ImGui::SliderFloat("Emission intensity", &m_Settings.emissionIntensity, 0.0f, 100.0f, "%.3f", ImGuiSliderFlags_Logarithmic);
                }
                ImGui::PopID();

                // "Hair" section
                if (m_SceneFile.find("Hair") != std::string::npos)
                {
                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader("HAIR", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("HAIR");
                    if (isUnfolded)
                    {
                        ImGui::SliderFloat2("Roughness", m_HairBetasOverride.pv, 0.01f, 1.0f, "%.3f");
                        ImGui::ColorEdit3("Color", m_HairBaseColorOverride.pv, ImGuiColorEditFlags_Float);
                    }
                    ImGui::PopID();
                }

                if (m_Settings.onScreen == 11)
                    ImGui::SliderFloat("Units in 1 meter", &m_Settings.meterToUnitsMultiplier, 0.001f, 100.0f, "%.4f", ImGuiSliderFlags_Logarithmic);
                else
                {
                    // "World" section
                    snprintf(buf, sizeof(buf) - 1, "WORLD%s", (m_Settings.animateSun || m_Settings.animatedObjects || m_Settings.animateScene) ? (m_Settings.pauseAnimation ? " (SPACE - unpause)" : " (SPACE - pause)") : "");

                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader(buf, ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("WORLD");
                    if (isUnfolded)
                    {
                        ImGui::Checkbox("Animate sun", &m_Settings.animateSun);
                        if (m_Scene.animations.size() > 0)
                        {
                            ImGui::SameLine();
                            ImGui::Checkbox("Animate scene", &m_Settings.animateScene);
                        }

                        if (m_Settings.animateSun || m_Settings.animatedObjects || m_Settings.animateScene)
                        {
                            ImGui::SameLine();
                            ImGui::Checkbox("Pause", &m_Settings.pauseAnimation);
                        }

                        ImGui::SameLine();
                        ImGui::SetNextItemWidth( ImGui::CalcItemWidth() - ImGui::GetCursorPosX() + ImGui::GetStyle().ItemSpacing.x );
                        ImGui::SliderFloat("Sun size (deg)", &m_Settings.sunAngularDiameter, 0.0f, 3.0f, "%.1f");

                        ImGui::SliderFloat2("Sun position (deg)", &m_Settings.sunAzimuth, -180.0f, 180.0f, "%.2f");
                        if (m_Settings.animateSun || m_Settings.animatedObjects || m_Settings.animateScene)
                            ImGui::SliderFloat("Slower / Faster", &m_Settings.animationSpeed, -10.0f, 10.0f);

                        ImGui::Checkbox("Objects", &m_Settings.animatedObjects);
                        if (m_Settings.animatedObjects)
                        {
                            ImGui::SameLine();
                            ImGui::Checkbox("9", &m_Settings.nineBrothers);
                            ImGui::SameLine();
                            ImGui::Checkbox("Blink", &m_Settings.blink);
                            ImGui::SameLine();
                            ImGui::Checkbox("Emissive", &m_Settings.emissiveObjects);
                            if (!m_Settings.nineBrothers)
                                ImGui::SliderInt("Object number", &m_Settings.animatedObjectNum, 1, (int32_t)MAX_ANIMATED_INSTANCE_NUM);
                            ImGui::SliderFloat("Object scale", &m_Settings.animatedObjectScale, 0.1f, 2.0f);
                        }

                        if (m_Settings.animateScene && m_Scene.animations[m_Settings.activeAnimation].durationMs != 0.0f)
                        {
                            char animationLabel[128];
                            snprintf(animationLabel, sizeof(animationLabel), "Animation %.1f sec (%%)", 0.001f * m_Scene.animations[m_Settings.activeAnimation].durationMs / (m_Settings.animationSpeed < 0.0f ? 1.0f / (1.0f + Abs(m_Settings.animationSpeed)) : (1.0f + m_Settings.animationSpeed)));
                            ImGui::SliderFloat(animationLabel, &m_Settings.animationProgress, 0.0f, 99.999f);

                            if (m_Scene.animations.size() > 1)
                            {
                                char items[1024] = {'\0'};
                                size_t offset = 0;
                                char* iterator = items;
                                for (auto animation : m_Scene.animations)
                                {
                                    const size_t size = std::min(sizeof(items), animation.name.length() + 1);
                                    memcpy(iterator + offset, animation.name.c_str(), size);
                                    offset += animation.name.length() + 1;
                                }
                                ImGui::Combo("Animated scene", &m_Settings.activeAnimation, items, helper::GetCountOf(m_Scene.animations));
                            }
                        }
                    }
                    ImGui::PopID();

                    // "Indirect rays" section
                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader("INDIRECT RAYS", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("INDIRECT RAYS");
                    if (isUnfolded)
                    {
                        const float sceneRadiusInMeters = m_Scene.aabb.GetRadius() / m_Settings.meterToUnitsMultiplier;

                        static const char* resolution[] =
                        {
                            "Full",
                            "Full (probabilistic)",
                            "Half",
                        };

                    #if( NRD_MODE == NORMAL || NRD_MODE == SH )
                        ImGui::SliderInt2("Samples / Bounces", &m_Settings.rpp, 1, 8);
                    #else
                        ImGui::SliderInt("Samples", &m_Settings.rpp, 1, 8);
                    #endif
                        ImGui::SliderFloat("AO / SO range (m)", &m_Settings.hitDistScale, 0.01f, sceneRadiusInMeters, "%.2f");
                        ImGui::PushStyleColor(ImGuiCol_Text, (m_Settings.denoiser == DENOISER_REFERENCE && m_Settings.tracingMode > RESOLUTION_FULL_PROBABILISTIC) ? UI_YELLOW : UI_DEFAULT);
                            ImGui::Combo("Resolution", &m_Settings.tracingMode, resolution, helper::GetCountOf(resolution));
                        ImGui::PopStyleColor();

                        ImGui::Checkbox("Diffuse", &m_Settings.indirectDiffuse);
                        ImGui::SameLine();
                        ImGui::Checkbox("Specular", &m_Settings.indirectSpecular);
                        ImGui::SameLine();
                        ImGui::Checkbox("Trim lobe", &m_Settings.specularLobeTrimming);
                        ImGui::SameLine();
                        ImGui::Checkbox("Normal map", &m_Settings.normalMap);

                    #if( NRD_MODE == NORMAL || NRD_MODE == SH )
                        const float3& sunDirection = GetSunDirection();
                        ImGui::PushStyleColor(ImGuiCol_Text, sunDirection.z > 0.0f ? UI_DEFAULT : (m_Settings.importanceSampling ? UI_GREEN : UI_YELLOW));
                        ImGui::Checkbox("IS", &m_Settings.importanceSampling);
                        ImGui::PopStyleColor();

                        ImGui::SameLine();
                        ImGui::Checkbox("Use prev frame", &m_Settings.usePrevFrame);
                        ImGui::SameLine();
                        ImGui::Checkbox("Ambient", &m_Settings.ambient);

                        if (m_Settings.tracingMode != RESOLUTION_HALF)
                            ImGui::SameLine();
                    #endif

                        if (m_Settings.tracingMode != RESOLUTION_HALF)
                        {
                            ImGui::PushStyleColor(ImGuiCol_Text, m_Settings.PSR ? UI_GREEN : UI_YELLOW);
                                ImGui::Checkbox("PSR", &m_Settings.PSR);
                            ImGui::PopStyleColor();
                        }
                    }
                    ImGui::PopID();

                    // "NRD" section
                    static const char* denoiser[] =
                    {
                    #if( NRD_MODE == OCCLUSION )
                        "REBLUR_OCCLUSION",
                        "(unsupported)",
                    #elif( NRD_MODE == SH )
                        "REBLUR_SH + SIGMA",
                        "RELAX_SH + SIGMA",
                    #elif( NRD_MODE == DIRECTIONAL_OCCLUSION )
                        "REBLUR_DIRECTIONAL_OCCLUSION",
                        "(unsupported)",
                    #else
                        "REBLUR + SIGMA",
                        "RELAX + SIGMA",
                    #endif
                        "REFERENCE",
                    };
                    const nrd::LibraryDesc& nrdLibraryDesc = nrd::GetLibraryDesc();
                    snprintf(buf, sizeof(buf) - 1, "NRD v%u.%u.%u (%u.%u) - %s [PgDown / PgUp]", nrdLibraryDesc.versionMajor, nrdLibraryDesc.versionMinor, nrdLibraryDesc.versionBuild,
                        nrdLibraryDesc.normalEncoding, nrdLibraryDesc.roughnessEncoding, denoiser[m_Settings.denoiser]);

                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader(buf, ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("NRD");
                    if (m_Settings.RR)
                        ImGui::Text("DLSS-RR is active. NRD is in passthrough mode...");
                    else if (isUnfolded)
                    {
                        static const char* hitDistanceReconstructionMode[] =
                        {
                            "Off",
                            "3x3",
                            "5x5",
                        };

                        if (m_DebugNRD)
                        {
                            ImGui::PushStyleColor(ImGuiCol_Text, m_ShowValidationOverlay ? UI_YELLOW : UI_DEFAULT);
                            ImGui::Checkbox("Validation overlay", &m_ShowValidationOverlay);
                            ImGui::PopStyleColor();
                        }

                        if (ImGui::Button("<<"))
                        {
                            m_Settings.denoiser--;
                            if (m_Settings.denoiser < DENOISER_REBLUR)
                                m_Settings.denoiser = DENOISER_REFERENCE;
                        }

                        ImGui::SameLine();
                        if (ImGui::Button(">>"))
                        {
                            m_Settings.denoiser++;
                            if (m_Settings.denoiser > DENOISER_REFERENCE)
                                m_Settings.denoiser = DENOISER_REBLUR;
                        }

                        ImGui::SameLine();
                        m_ForceHistoryReset = ImGui::Button("Reset");

                        if (m_Settings.denoiser == DENOISER_REBLUR)
                        {
                            nrd::ReblurSettings defaults = GetDefaultReblurSettings();

                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                            {
                                defaults.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3;
                                defaults.diffusePrepassBlurRadius = defaults.specularPrepassBlurRadius;
                            }

                            // Helps to mitigate fireflies emphasized by DLSS
                            #if( NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION )
                                defaults.enableAntiFirefly = m_DlssQuality != -1 && IsDlssEnabled();
                            #endif

                            bool isSame = true;
                            if (m_ReblurSettings.antilagSettings.luminanceSigmaScale != defaults.antilagSettings.luminanceSigmaScale)
                                isSame = false;
                            else if (m_ReblurSettings.antilagSettings.hitDistanceSigmaScale != defaults.antilagSettings.hitDistanceSigmaScale)
                                isSame = false;
                            else if (m_ReblurSettings.antilagSettings.luminanceAntilagPower != defaults.antilagSettings.luminanceAntilagPower)
                                isSame = false;
                            else if (m_ReblurSettings.antilagSettings.hitDistanceAntilagPower != defaults.antilagSettings.hitDistanceAntilagPower)
                                isSame = false;
                            else if (m_ReblurSettings.historyFixFrameNum != defaults.historyFixFrameNum)
                                isSame = false;
                            else if (m_ReblurSettings.minBlurRadius != defaults.minBlurRadius)
                                isSame = false;
                            else if (m_ReblurSettings.maxBlurRadius != defaults.maxBlurRadius)
                                isSame = false;
                            else if (m_ReblurSettings.diffusePrepassBlurRadius != defaults.diffusePrepassBlurRadius)
                                isSame = false;
                            else if (m_ReblurSettings.specularPrepassBlurRadius != defaults.specularPrepassBlurRadius)
                                isSame = false;
                            else if (m_ReblurSettings.lobeAngleFraction != defaults.lobeAngleFraction)
                                isSame = false;
                            else if (m_ReblurSettings.roughnessFraction != defaults.roughnessFraction)
                                isSame = false;
                            else if (m_ReblurSettings.responsiveAccumulationRoughnessThreshold != defaults.responsiveAccumulationRoughnessThreshold)
                                isSame = false;
                            else if (m_ReblurSettings.stabilizationStrength != defaults.stabilizationStrength)
                                isSame = false;
                            else if (m_ReblurSettings.planeDistanceSensitivity != defaults.planeDistanceSensitivity)
                                isSame = false;
                            else if (m_ReblurSettings.hitDistanceReconstructionMode != defaults.hitDistanceReconstructionMode)
                                isSame = false;
                            else if (m_ReblurSettings.enableAntiFirefly != defaults.enableAntiFirefly)
                                isSame = false;
                            else if (m_ReblurSettings.enablePerformanceMode != defaults.enablePerformanceMode)
                                isSame = false;
                            else if (m_ReblurSettings.usePrepassOnlyForSpecularMotionEstimation != defaults.usePrepassOnlyForSpecularMotionEstimation)
                                isSame = false;

                            ImGui::SameLine();
                            if (ImGui::Button("No spatial"))
                            {
                                m_ReblurSettings.minBlurRadius = 0.0f;
                                m_ReblurSettings.maxBlurRadius = 0.0f;
                                m_ReblurSettings.diffusePrepassBlurRadius = 0.0f;
                                m_ReblurSettings.specularPrepassBlurRadius = 0.0f;
                            }

                            ImGui::SameLine();
                            if (ImGui::Button(m_Settings.maxFastAccumulatedFrameNum < m_Settings.maxAccumulatedFrameNum ? "No fast" : "Fast"))
                            {
                                if (m_Settings.maxFastAccumulatedFrameNum < m_Settings.maxAccumulatedFrameNum)
                                    m_Settings.maxFastAccumulatedFrameNum = MAX_HISTORY_FRAME_NUM;
                                else
                                    m_Settings.maxFastAccumulatedFrameNum = defaults.maxFastAccumulatedFrameNum;
                            }

                            ImGui::SameLine();
                            ImGui::PushStyleColor(ImGuiCol_Text, isSame ? UI_DEFAULT : UI_YELLOW);
                            if (ImGui::Button("Defaults") || frameIndex == 0)
                                m_ReblurSettings = defaults;
                            ImGui::PopStyleColor();

                            ImGui::Checkbox("Adaptive accumulation", &m_Settings.adaptiveAccumulation);
                            ImGui::SameLine();
                            ImGui::Checkbox("Anti-firefly", &m_ReblurSettings.enableAntiFirefly);

                            ImGui::Checkbox("Performance mode", &m_ReblurSettings.enablePerformanceMode);
                        #if( NRD_MODE == SH || NRD_MODE == DIRECTIONAL_OCCLUSION )
                            ImGui::SameLine();
                            ImGui::PushStyleColor(ImGuiCol_Text, m_Resolve ? UI_GREEN : UI_RED);
                                ImGui::Checkbox("Resolve", &m_Resolve);
                            ImGui::PopStyleColor();
                        #endif

                            ImGui::SliderFloat("Disocclusion (%)", &m_Settings.disocclusionThreshold, 0.25f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
                            ImGui::SliderInt2("History length (frames)", &m_Settings.maxAccumulatedFrameNum, 0, MAX_HISTORY_FRAME_NUM, "%d");

                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                            {
                                ImGui::PushStyleColor(ImGuiCol_Text, m_ReblurSettings.hitDistanceReconstructionMode != nrd::HitDistanceReconstructionMode::OFF ? UI_GREEN : UI_RED);
                                {
                                    int32_t v = (int32_t)m_ReblurSettings.hitDistanceReconstructionMode;
                                    ImGui::Combo("HitT reconstruction", &v, hitDistanceReconstructionMode, helper::GetCountOf(hitDistanceReconstructionMode));
                                    m_ReblurSettings.hitDistanceReconstructionMode = (nrd::HitDistanceReconstructionMode)v;
                                }
                                ImGui::PopStyleColor();
                            }

                        #if( NRD_MODE != OCCLUSION )
                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                                ImGui::PushStyleColor(ImGuiCol_Text, m_ReblurSettings.diffusePrepassBlurRadius != 0.0f && m_ReblurSettings.specularPrepassBlurRadius != 0.0f ? UI_GREEN : UI_RED);
                            ImGui::SliderFloat2("Pre-pass radius (px)", &m_ReblurSettings.diffusePrepassBlurRadius, 0.0f, 75.0f, "%.1f");
                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                                ImGui::PopStyleColor();
                        #endif

                            ImGui::PushStyleColor(ImGuiCol_Text, m_ReblurSettings.minBlurRadius < 0.5f ? UI_RED : UI_DEFAULT);
                            ImGui::SliderFloat("Min blur radius (px)", &m_ReblurSettings.minBlurRadius, 0.0f, 5.0f, "%.1f");
                            ImGui::PopStyleColor();

                            ImGui::SliderFloat("Max blur radius (px)", &m_ReblurSettings.maxBlurRadius, 0.0f, 60.0f, "%.1f");
                            ImGui::SliderFloat("Lobe fraction", &m_ReblurSettings.lobeAngleFraction, 0.0f, 1.0f, "%.2f");
                            ImGui::SliderFloat("Roughness fraction", &m_ReblurSettings.roughnessFraction, 0.0f, 1.0f, "%.2f");
                            ImGui::SliderInt("History fix frames", (int32_t*)&m_ReblurSettings.historyFixFrameNum, 0, 3);
                        #if( NRD_MODE != OCCLUSION )
                            ImGui::SliderFloat("Stabilization (%)", &m_ReblurSettings.stabilizationStrength, 0.0f, 1.0f, "%.2f");
                        #endif
                            ImGui::SetNextItemWidth( ImGui::CalcItemWidth() * 0.5f );
                            ImGui::SliderFloat("Responsive accumulation roughness threshold", &m_ReblurSettings.responsiveAccumulationRoughnessThreshold, 0.0f, 1.0f, "%.2f");

                            if (m_ReblurSettings.stabilizationStrength != 0.0f)
                            {
                                ImGui::Text("ANTI-LAG (luminance & hit distance):");
                                ImGui::SliderFloat2("Sigma scale", &m_ReblurSettings.antilagSettings.luminanceSigmaScale, 1.0f, 3.0f, "%.1f");
                                ImGui::SliderFloat2("Power", &m_ReblurSettings.antilagSettings.luminanceAntilagPower, 0.01f, 1.0f, "%.2f");
                            }
                        }
                        else if (m_Settings.denoiser == DENOISER_RELAX)
                        {
                            nrd::RelaxSettings defaults = GetDefaultRelaxSettings();

                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                            {
                                defaults.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3;
                                defaults.diffusePrepassBlurRadius = defaults.specularPrepassBlurRadius;
                            }

                            // Helps to mitigate fireflies emphasized by DLSS
                            #if( NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION )
                                //defaults.enableAntiFirefly = m_DlssQuality != -1 && IsDlssEnabled(); // TODO: currently doesn't help in this case, but makes the image darker
                            #endif

                            bool isSame = true;
                            if (m_RelaxSettings.antilagSettings.accelerationAmount != defaults.antilagSettings.accelerationAmount)
                                isSame = false;
                            else if (m_RelaxSettings.antilagSettings.spatialSigmaScale != defaults.antilagSettings.spatialSigmaScale)
                                isSame = false;
                            else if (m_RelaxSettings.antilagSettings.temporalSigmaScale != defaults.antilagSettings.temporalSigmaScale)
                                isSame = false;
                            else if (m_RelaxSettings.antilagSettings.resetAmount != defaults.antilagSettings.resetAmount)
                                isSame = false;
                            else if (m_RelaxSettings.diffusePrepassBlurRadius != defaults.diffusePrepassBlurRadius)
                                isSame = false;
                            else if (m_RelaxSettings.specularPrepassBlurRadius != defaults.specularPrepassBlurRadius)
                                isSame = false;
                            else if (m_RelaxSettings.historyFixFrameNum != defaults.historyFixFrameNum)
                                isSame = false;
                            else if (m_RelaxSettings.diffusePhiLuminance != defaults.diffusePhiLuminance)
                                isSame = false;
                            else if (m_RelaxSettings.specularPhiLuminance != defaults.specularPhiLuminance)
                                isSame = false;
                            else if (m_RelaxSettings.diffuseLobeAngleFraction != defaults.diffuseLobeAngleFraction)
                                isSame = false;
                            else if (m_RelaxSettings.specularLobeAngleFraction != defaults.specularLobeAngleFraction)
                                isSame = false;
                            else if (m_RelaxSettings.roughnessFraction != defaults.roughnessFraction)
                                isSame = false;
                            else if (m_RelaxSettings.specularVarianceBoost != defaults.specularVarianceBoost)
                                isSame = false;
                            else if (m_RelaxSettings.specularLobeAngleSlack != defaults.specularLobeAngleSlack)
                                isSame = false;
                            else if (m_RelaxSettings.historyFixEdgeStoppingNormalPower != defaults.historyFixEdgeStoppingNormalPower)
                                isSame = false;
                            else if (m_RelaxSettings.historyClampingColorBoxSigmaScale != defaults.historyClampingColorBoxSigmaScale)
                                isSame = false;
                            else if (m_RelaxSettings.spatialVarianceEstimationHistoryThreshold != defaults.spatialVarianceEstimationHistoryThreshold)
                                isSame = false;
                            else if (m_RelaxSettings.atrousIterationNum != defaults.atrousIterationNum)
                                isSame = false;
                            else if (m_RelaxSettings.diffuseMinLuminanceWeight != defaults.diffuseMinLuminanceWeight)
                                isSame = false;
                            else if (m_RelaxSettings.specularMinLuminanceWeight != defaults.specularMinLuminanceWeight)
                                isSame = false;
                            else if (m_RelaxSettings.depthThreshold != defaults.depthThreshold)
                                isSame = false;
                            else if (m_RelaxSettings.confidenceDrivenRelaxationMultiplier != defaults.confidenceDrivenRelaxationMultiplier)
                                isSame = false;
                            else if (m_RelaxSettings.confidenceDrivenLuminanceEdgeStoppingRelaxation != defaults.confidenceDrivenLuminanceEdgeStoppingRelaxation)
                                isSame = false;
                            else if (m_RelaxSettings.confidenceDrivenNormalEdgeStoppingRelaxation != defaults.confidenceDrivenNormalEdgeStoppingRelaxation)
                                isSame = false;
                            else if (m_RelaxSettings.luminanceEdgeStoppingRelaxation != defaults.luminanceEdgeStoppingRelaxation)
                                isSame = false;
                            else if (m_RelaxSettings.normalEdgeStoppingRelaxation != defaults.normalEdgeStoppingRelaxation)
                                isSame = false;
                            else if (m_RelaxSettings.roughnessEdgeStoppingRelaxation != defaults.roughnessEdgeStoppingRelaxation)
                                isSame = false;
                            else if (m_RelaxSettings.hitDistanceReconstructionMode != defaults.hitDistanceReconstructionMode)
                                isSame = false;
                            else if (m_RelaxSettings.enableAntiFirefly != defaults.enableAntiFirefly)
                                isSame = false;
                            else if (m_RelaxSettings.enableRoughnessEdgeStopping != defaults.enableRoughnessEdgeStopping)
                                isSame = false;

                            ImGui::SameLine();
                            if (ImGui::Button("No spatial"))
                            {
                                m_RelaxSettings.diffusePhiLuminance = 0.0f;
                                m_RelaxSettings.specularPhiLuminance = 0.0f;
                                m_RelaxSettings.diffusePrepassBlurRadius = 0.0f;
                                m_RelaxSettings.specularPrepassBlurRadius = 0.0f;
                                m_RelaxSettings.spatialVarianceEstimationHistoryThreshold = 0;
                            }

                            ImGui::SameLine();
                            if (ImGui::Button(m_Settings.maxFastAccumulatedFrameNum < m_Settings.maxAccumulatedFrameNum ? "No fast" : "Fast"))
                            {
                                if (m_Settings.maxFastAccumulatedFrameNum < m_Settings.maxAccumulatedFrameNum)
                                    m_Settings.maxFastAccumulatedFrameNum = MAX_HISTORY_FRAME_NUM;
                                else
                                    m_Settings.maxFastAccumulatedFrameNum = defaults.diffuseMaxFastAccumulatedFrameNum;
                            }

                            ImGui::SameLine();
                            ImGui::PushStyleColor(ImGuiCol_Text, isSame ? UI_DEFAULT : UI_YELLOW);
                            if (ImGui::Button("Defaults") || frameIndex == 0)
                                m_RelaxSettings = defaults;
                            ImGui::PopStyleColor();

                            ImGui::Checkbox("Adaptive accumulation", &m_Settings.adaptiveAccumulation);
                            ImGui::SameLine();
                            ImGui::Checkbox("Anti-firefly", &m_RelaxSettings.enableAntiFirefly);

                            ImGui::Checkbox("Roughness edge stopping", &m_RelaxSettings.enableRoughnessEdgeStopping);
                        #if( NRD_MODE == SH)
                            ImGui::SameLine();
                            ImGui::PushStyleColor(ImGuiCol_Text, m_Resolve ? UI_GREEN : UI_RED);
                                ImGui::Checkbox("Resolve", &m_Resolve);
                            ImGui::PopStyleColor();
                        #endif

                            ImGui::SliderFloat("Disocclusion (%)", &m_Settings.disocclusionThreshold, 0.25f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
                            ImGui::SliderInt2("History length (frames)", &m_Settings.maxAccumulatedFrameNum, 0, MAX_HISTORY_FRAME_NUM, "%d");

                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                            {
                                ImGui::PushStyleColor(ImGuiCol_Text, m_RelaxSettings.hitDistanceReconstructionMode != nrd::HitDistanceReconstructionMode::OFF ? UI_GREEN : UI_RED);
                                {
                                    int32_t v = (int32_t)m_RelaxSettings.hitDistanceReconstructionMode;
                                    ImGui::Combo("HitT reconstruction", &v, hitDistanceReconstructionMode, helper::GetCountOf(hitDistanceReconstructionMode));
                                    m_RelaxSettings.hitDistanceReconstructionMode = (nrd::HitDistanceReconstructionMode)v;
                                }
                                ImGui::PopStyleColor();
                            }

                        #if( NRD_MODE != OCCLUSION )
                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                                ImGui::PushStyleColor(ImGuiCol_Text, m_RelaxSettings.diffusePrepassBlurRadius != 0.0f && m_RelaxSettings.specularPrepassBlurRadius != 0.0f ? UI_GREEN : UI_RED);
                            ImGui::SliderFloat2("Pre-pass radius (px)", &m_RelaxSettings.diffusePrepassBlurRadius, 0.0f, 75.0f, "%.1f");
                            if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
                                ImGui::PopStyleColor();
                        #endif

                            ImGui::SliderInt("A-trous iterations", (int32_t*)&m_RelaxSettings.atrousIterationNum, 2, 8);
                            ImGui::SliderFloat2("Diff-Spec luma weight", &m_RelaxSettings.diffusePhiLuminance, 0.0f, 10.0f, "%.1f");
                            ImGui::SliderFloat2("Min luma weight", &m_RelaxSettings.diffuseMinLuminanceWeight, 0.0f, 1.0f, "%.2f");
                            ImGui::SliderFloat("Spec lobe angle slack", &m_RelaxSettings.specularLobeAngleSlack, 0.0f, 89.0f, "%.3f", ImGuiSliderFlags_Logarithmic);
                            ImGui::SliderFloat("Depth threshold", &m_RelaxSettings.depthThreshold, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Logarithmic);
                            ImGui::Text("Diffuse lobe / Specular lobe / Roughness:");
                            ImGui::SliderFloat3("Fraction", &m_RelaxSettings.diffuseLobeAngleFraction, 0.0f, 1.0f, "%.2f");
                            ImGui::Text("Luminance / Normal / Roughness:");
                            ImGui::SliderFloat3("Relaxation", &m_RelaxSettings.luminanceEdgeStoppingRelaxation, 0.0f, 1.0f, "%.2f");
                            ImGui::SliderFloat("Spec variance boost", &m_RelaxSettings.specularVarianceBoost, 0.0f, 8.0f, "%.2f");
                            ImGui::SliderFloat("Clamping sigma scale", &m_RelaxSettings.historyClampingColorBoxSigmaScale, 0.0f, 10.0f, "%.1f");
                            ImGui::SliderInt("History threshold", (int32_t*)&m_RelaxSettings.spatialVarianceEstimationHistoryThreshold, 0, 10);

                            ImGui::Text("HISTORY FIX:");
                            ImGui::SliderFloat("Normal weight power", &m_RelaxSettings.historyFixEdgeStoppingNormalPower, 0.0f, 128.0f, "%.1f");
                            ImGui::SliderInt("Frames", (int32_t*)&m_RelaxSettings.historyFixFrameNum, 0, 3);

                            ImGui::Text("ANTI-LAG:");
                            ImGui::SliderFloat("Acceleration amount", &m_RelaxSettings.antilagSettings.accelerationAmount, 0.0f, 1.0f, "%.2f");
                            ImGui::SliderFloat("Spatial sigma scale", &m_RelaxSettings.antilagSettings.spatialSigmaScale, 0.0f, 10.0f, "%.1f");
                            ImGui::SliderFloat("Temporal sigma scale", &m_RelaxSettings.antilagSettings.temporalSigmaScale, 0.0f, 10.0f, "%.1f");
                            ImGui::SliderFloat("Reset amount", &m_RelaxSettings.antilagSettings.resetAmount, 0.0f, 1.0f, "%.2f");
                        }
                        else if (m_Settings.denoiser == DENOISER_REFERENCE)
                        {
                            float t = (float)m_ReferenceSettings.maxAccumulatedFrameNum;
                            ImGui::SliderFloat("History length (frames)", &t, 0.0f, 1024.0f, "%.0f", ImGuiSliderFlags_Logarithmic);
                            m_ReferenceSettings.maxAccumulatedFrameNum = (int32_t)t;
                        }
                    }
                    ImGui::PopID();

                    // "Other" section
                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader("OTHER", ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_DefaultOpen);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("OTHER");
                    if (isUnfolded)
                    {
                        ImGui::SliderFloat("Debug [F1]", &m_Settings.debug, 0.0f, 1.0f, "%.6f");
                        ImGui::SliderFloat("Input / Denoised", &m_Settings.separator, 0.0f, 1.0f, "%.2f");

                        if (ImGui::Button(m_Settings.windowAlignment ? ">>" : "<<"))
                            m_Settings.windowAlignment = !m_Settings.windowAlignment;

                        ImGui::SameLine();
                        if (ImGui::Button("Reload shaders"))
                        {
                            int result = 0;
                            #ifdef _WIN32 // TODO: can be made Linux friendly too
                                #ifdef _DEBUG
                                    std::string sampleShaders = "_Bin\\Debug\\ShaderMake.exe";
                                    std::string nrdShaders = "_Bin\\Debug\\ShaderMake.exe";
                                #else
                                    std::string sampleShaders = "_Bin\\Release\\ShaderMake.exe";
                                    std::string nrdShaders = "_Bin\\Release\\ShaderMake.exe";
                                #endif

                                sampleShaders +=
                                    " --useAPI --binary --flatten --stripReflection --WX --colorize"
                                    " -c Shaders.cfg -o _Shaders --sourceDir Shaders"
                                    " -I Shaders -I External -I External/NGX -I External/NRD/External -I External/NRIFramework/Shaders"
                                    " -D COMPILER_DXC -D NRD_NORMAL_ENCODING=" STRINGIFY(NRD_NORMAL_ENCODING) " -D NRD_ROUGHNESS_ENCODING=" STRINGIFY(NRD_ROUGHNESS_ENCODING);

                                nrdShaders +=
                                    " --useAPI --header --binary --flatten --stripReflection --WX --allResourcesBound --colorize"
                                    " -c External/NRD/Shaders.cfg -o _Shaders --sourceDir Shaders/Source"
                                    " -I External/MathLib -I Shaders/Include -I Shaders/Resources"
                                    " -D NRD_INTERNAL -D NRD_NORMAL_ENCODING=" STRINGIFY(NRD_NORMAL_ENCODING) " -D NRD_ROUGHNESS_ENCODING=" STRINGIFY(NRD_ROUGHNESS_ENCODING);

                                if (NRI.GetDeviceDesc(*m_Device).graphicsAPI == nri::GraphicsAPI::D3D12)
                                {
                                    std::string dxil = " -p DXIL --compiler \"" STRINGIFY(DXC_PATH) "\"";
                                    sampleShaders += dxil;
                                    nrdShaders += dxil;
                                }
                                else
                                {
                                    std::string spirv = " -p SPIRV --compiler \"" STRINGIFY(DXC_SPIRV_PATH) "\" -D VULKAN --hlsl2021 --sRegShift 100 --tRegShift 200 --bRegShift 300 --uRegShift 400";
                                    sampleShaders += spirv;
                                    nrdShaders += spirv;
                                }

                                printf("Compiling sample shaders...\n");
                                result = system(sampleShaders.c_str());
                                if (!result)
                                {
                                    printf("Compiling NRD shaders...\n");
                                    result = system(nrdShaders.c_str());
                                }

                                if (result)
                                    SetForegroundWindow(GetConsoleWindow());

                                #undef SAMPLE_SHADERS
                                #undef NRD_SHADERS
                            #endif

                            if (!result)
                                CreatePipelines();

                            printf("Ready!\n");
                        }

                        ImGui::SameLine();
                        if (ImGui::Button("Defaults"))
                        {
                            m_Camera.Initialize(m_Scene.aabb.GetCenter(), m_Scene.aabb.vMin, CAMERA_RELATIVE);
                            m_Settings = m_SettingsDefault;
                            m_RelaxSettings = GetDefaultRelaxSettings();
                            m_ReblurSettings = GetDefaultReblurSettings();
                            m_ForceHistoryReset = true;
                        }
                    }
                    ImGui::PopID();

                    // "Tests" section
                    ImGui::NewLine();
                    ImGui::PushStyleColor(ImGuiCol_Text, UI_HEADER);
                    ImGui::PushStyleColor(ImGuiCol_Header, UI_HEADER_BACKGROUND);
                    isUnfolded = ImGui::CollapsingHeader("TESTS [F2]", ImGuiTreeNodeFlags_CollapsingHeader);
                    ImGui::PopStyleColor();
                    ImGui::PopStyleColor();

                    ImGui::PushID("TESTS");
                    if (isUnfolded)
                    {
                        float buttonWidth = 25.0f * float(GetWindowResolution().x) / float(GetOutputResolution().x);

                        char s[64];
                        std::string sceneName = std::string( utils::GetFileName(m_SceneFile) );
                        size_t dotPos = sceneName.find_last_of(".");
                        if (dotPos != std::string::npos)
                            sceneName = sceneName.substr(0, dotPos) + ".bin";
                        const std::string path = utils::GetFullPath(sceneName, utils::DataFolder::TESTS);
                        const uint32_t testByteSize = sizeof(m_Settings) + Camera::GetStateSize();

                        // Get number of tests
                        if (m_TestNum == uint32_t(-1))
                        {
                            FILE* fp = fopen(path.c_str(), "rb");
                            if (fp)
                            {
                                // Use this code to convert tests to reflect new Settings and Camera layouts
                                #if 0
                                    typedef Settings SettingsOld; // adjust if needed
                                    typedef Camera CameraOld; // adjust if needed

                                    const uint32_t oldItemSize = sizeof(SettingsOld) + CameraOld::GetStateSize();

                                    fseek(fp, 0, SEEK_END);
                                    m_TestNum = ftell(fp) / oldItemSize;
                                    fseek(fp, 0, SEEK_SET);

                                    FILE* fpNew;
                                    fopen_s(&fpNew, (path + ".new").c_str(), "wb");

                                    for (uint32_t i = 0; i < m_TestNum && fpNew; i++)
                                    {
                                        SettingsOld settingsOld;
                                        fread_s(&settingsOld, sizeof(SettingsOld), 1, sizeof(SettingsOld), fp);

                                        CameraOld cameraOld;
                                        fread_s(cameraOld.GetState(), CameraOld::GetStateSize(), 1, CameraOld::GetStateSize(), fp);

                                        // Convert Old to New here
                                        m_Settings = settingsOld;
                                        m_Camera.state = cameraOld.state;

                                        // ...

                                        fwrite(&m_Settings, 1, sizeof(m_Settings), fpNew);
                                        fwrite(m_Camera.GetState(), 1, Camera::GetStateSize(), fpNew);
                                    }

                                    fclose(fp);
                                    fclose(fpNew);

                                    __debugbreak();
                                #endif

                                fseek(fp, 0, SEEK_END);
                                m_TestNum = ftell(fp) / testByteSize;
                                fclose(fp);
                            }
                            else
                                m_TestNum = 0;
                        }

                        // Adjust current test index
                        bool isTestChanged = false;
                        if (IsKeyToggled(Key::F2) && m_TestNum)
                        {
                            m_LastSelectedTest++;
                            isTestChanged = true;
                        }

                        if (m_LastSelectedTest == uint32_t(-1) || !m_TestNum)
                            m_LastSelectedTest = uint32_t(-1);
                        else
                            m_LastSelectedTest %= m_TestNum;

                        // Main buttons
                        uint32_t i = 0;
                        for (; i < m_TestNum; i++)
                        {
                            snprintf(s, sizeof(s), "%u", i + 1);

                            if (i % 14 != 0)
                                ImGui::SameLine();

                            bool isColorChanged = false;
                            if(m_improveMeTests && std::find(m_improveMeTests->begin(), m_improveMeTests->end(), i + 1) != m_improveMeTests->end())
                            {
                                ImGui::PushStyleColor(ImGuiCol_Text, UI_RED);
                                isColorChanged = true;
                            }
                            else if(m_checkMeTests && std::find(m_checkMeTests->begin(), m_checkMeTests->end(), i + 1) != m_checkMeTests->end())
                            {
                                ImGui::PushStyleColor(ImGuiCol_Text, UI_YELLOW);
                                isColorChanged = true;
                            }

                            if (ImGui::Button(i == m_LastSelectedTest ? "*" : s, ImVec2(buttonWidth, 0.0f)) || isTestChanged)
                            {
                                uint32_t test = isTestChanged ? m_LastSelectedTest : i;
                                FILE* fp = fopen(path.c_str(), "rb");

                                if (fp && fseek(fp, test * testByteSize, SEEK_SET) == 0)
                                {
                                    size_t elemNum = fread(&m_Settings, sizeof(m_Settings), 1, fp);
                                    if (elemNum == 1)
                                        elemNum = fread(m_Camera.GetState(), Camera::GetStateSize(), 1, fp);

                                    m_LastSelectedTest = test;

                                    // File read error
                                    if (elemNum != 1)
                                    {
                                        m_Camera.Initialize(m_Scene.aabb.GetCenter(), m_Scene.aabb.vMin, CAMERA_RELATIVE);
                                        m_Settings = m_SettingsDefault;
                                    }

                                    // Reset some settings to defaults to avoid a potential confusion
                                    m_Settings.debug = 0.0f;
                                    m_Settings.denoiser = DENOISER_REBLUR;
                                    m_Settings.RR = m_DLSS.HasRR();
                                    m_Settings.SR = m_DLSS.HasSR();
                                    m_Settings.TAA = true;
                                    m_Settings.NIS = true;
                                    m_Settings.cameraJitter = true;
                                    m_ForceHistoryReset = true;
                                }

                                if (fp)
                                    fclose(fp);

                                isTestChanged = false;
                            }

                            if (isColorChanged)
                                ImGui::PopStyleColor();
                        }

                        if (i % 14 != 0)
                            ImGui::SameLine();

                        // "Add" button
                        if (ImGui::Button("Add"))
                        {
                            FILE* fp = fopen(path.c_str(), "ab");

                            if (fp)
                            {
                                m_Settings.motionStartTime = m_Settings.motionStartTime > 0.0 ? -1.0 : 0.0;

                                fwrite(&m_Settings, sizeof(m_Settings), 1, fp);
                                fwrite(m_Camera.GetState(), Camera::GetStateSize(), 1, fp);
                                fclose(fp);

                                m_TestNum = uint32_t(-1);
                            }
                        }

                        if ((i + 1) % 14 != 0)
                            ImGui::SameLine();

                        // "Del" button
                        snprintf(s, sizeof(s), "Del %u", m_LastSelectedTest + 1);
                        if (m_TestNum != uint32_t(-1) && m_LastSelectedTest != uint32_t(-1) && ImGui::Button(s))
                        {
                            std::vector<uint8_t> data;
                            utils::LoadFile(path, data);

                            FILE* fp = fopen(path.c_str(), "wb");

                            if (fp)
                            {
                                for (i = 0; i < m_TestNum; i++)
                                {
                                    if (i != m_LastSelectedTest)
                                        fwrite(&data[i * testByteSize], 1, testByteSize, fp);
                                }

                                fclose(fp);

                                m_TestNum = uint32_t(-1);
                            }
                        }
                    }
                    ImGui::PopID();
                }
            }
            m_UiWidth = ImGui::GetWindowWidth();
        }
        ImGui::End();
    }
    EndUI(NRI, *m_Streamer);

    // Animate scene and update camera
    cBoxf cameraLimits = m_Scene.aabb;
    cameraLimits.Scale(2.0f);

    CameraDesc desc = {};
    desc.limits = cameraLimits;
    desc.aspectRatio = float(GetOutputResolution().x) / float(GetOutputResolution().y);
    desc.horizontalFov = RadToDeg( Atan( Tan( DegToRad( m_Settings.camFov ) * 0.5f ) *  desc.aspectRatio * 9.0f / 16.0f ) * 2.0f ); // recalculate to ultra-wide if needed
    desc.nearZ = NEAR_Z * m_Settings.meterToUnitsMultiplier;
    desc.farZ = 10000.0f * m_Settings.meterToUnitsMultiplier;
    desc.isCustomMatrixSet = false; // No camera animation hooked up
    desc.isPositiveZ = m_PositiveZ;
    desc.isReversedZ = m_ReversedZ;
    desc.orthoRange = m_Settings.ortho ? Tan( DegToRad( m_Settings.camFov ) * 0.5f ) * 3.0f * m_Settings.meterToUnitsMultiplier : 0.0f;
    desc.backwardOffset = CAMERA_BACKWARD_OFFSET;
    GetCameraDescFromInputDevices(desc);

    if (m_Settings.motionStartTime > 0.0)
    {
        float time = float(m_Timer.GetTimeStamp() - m_Settings.motionStartTime);
        float amplitude = 40.0f * m_Camera.state.motionScale;
        float period = 0.0003f * time * (m_Settings.emulateMotionSpeed < 0.0f ? 1.0f / (1.0f + Abs(m_Settings.emulateMotionSpeed)) : (1.0f + m_Settings.emulateMotionSpeed));

        float3 localPos = m_Camera.state.mWorldToView.GetRow0().To3d();
        if (m_Settings.motionMode == 1)
            localPos = m_Camera.state.mWorldToView.GetRow1().To3d();
        else if (m_Settings.motionMode == 2)
            localPos = m_Camera.state.mWorldToView.GetRow2().To3d();
        else if (m_Settings.motionMode == 3)
        {
            float3 rows[3] = { m_Camera.state.mWorldToView.GetRow0().To3d(), m_Camera.state.mWorldToView.GetRow1().To3d(), m_Camera.state.mWorldToView.GetRow2().To3d() };
            float f = Sin( Pi(period * 3.0f) );
            localPos = Normalize( f < 0.0f ? Lerp( rows[1], rows[0], float3( Abs(f) ) ) : Lerp( rows[1], rows[2], float3(f) ) );
        }

        if (m_Settings.motionMode == 4)
        {
            float3 axisX = m_Camera.state.mWorldToView.GetRow0().To3d();
            float3 axisY = m_Camera.state.mWorldToView.GetRow1().To3d();
            float2 v = Rotate(float2(1.0f, 0.0f), Mod(Pi(period * 2.0f), Pi(2.0f)));
            localPos = (axisX * v.x + axisY * v.y) * amplitude / Pi(1.0f);
        }
        else
            localPos *= amplitude * (m_Settings.linearMotion ? WaveTriangle(period) - 0.5f : Sin( Pi(period) ) * 0.5f);

        desc.dUser = localPos - m_PrevLocalPos;
        m_PrevLocalPos = localPos;
    }
    else if (m_Settings.motionStartTime == -1.0)
    {
        m_Settings.motionStartTime = m_Timer.GetTimeStamp();
        m_PrevLocalPos = float3::Zero();
    }

    m_Camera.Update(desc, frameIndex);

    // Animate scene
    const float animationSpeed = m_Settings.pauseAnimation ? 0.0f : (m_Settings.animationSpeed < 0.0f ? 1.0f / (1.0f + Abs(m_Settings.animationSpeed)) : (1.0f + m_Settings.animationSpeed));
    const float animationDelta = animationSpeed * m_Timer.GetFrameTime() * 0.001f;

    for (size_t i = 0; i < m_Scene.animations.size(); i++)
        m_Scene.Animate(animationSpeed, m_Timer.GetFrameTime(), m_Settings.animationProgress, (int32_t)i);

    // Animate sun
    if (m_Settings.animateSun)
    {
        static float sunAzimuthPrev = 0.0f;
        static double sunMotionStartTime = 0.0;
        if (m_Settings.animateSun != m_SettingsPrev.animateSun)
        {
            sunAzimuthPrev = m_Settings.sunAzimuth;
            sunMotionStartTime = m_Timer.GetTimeStamp();
        }
        double t = m_Timer.GetTimeStamp() - sunMotionStartTime;
        if (!m_Settings.pauseAnimation)
            m_Settings.sunAzimuth = sunAzimuthPrev + (float)Sin(t * animationSpeed * 0.0003) * 10.0f;
    }

    // Animate objects
    const float scale = m_Settings.animatedObjectScale * m_Settings.meterToUnitsMultiplier / 2.0f;
    if (m_Settings.nineBrothers)
    {
        m_Settings.animatedObjectNum = 9;

        const float3& vRight = m_Camera.state.mViewToWorld.GetCol0().xmm;
        const float3& vTop = m_Camera.state.mViewToWorld.GetCol1().xmm;
        const float3& vForward = m_Camera.state.mViewToWorld.GetCol2().xmm;

        float3 basePos = ToFloat(m_Camera.state.globalPosition);

        for (int32_t i = -1; i <= 1; i++ )
        {
            for (int32_t j = -1; j <= 1; j++ )
            {
                const uint32_t index = (i + 1) * 3 + (j + 1);

                float x = float(i) * scale * 4.0f;
                float y = float(j) * scale * 4.0f;
                float z = 10.0f * scale * (m_PositiveZ ? 1.0f : -1.0f);

                float3 pos = basePos + vRight * x + vTop * y + vForward * z;

                utils::Instance& instance = m_Scene.instances[ m_AnimatedInstances[index].instanceID ];
                instance.position = ToDouble( pos );
                instance.rotation = m_Camera.state.mViewToWorld;
                instance.rotation.SetTranslation( float3::Zero() );
                instance.rotation.AddScale(scale);
            }
        }
    }
    else if (m_Settings.animatedObjects)
    {
        for (int32_t i = 0; i < m_Settings.animatedObjectNum; i++)
        {
            float3 position;
            float4x4 transform = m_AnimatedInstances[i].Animate(animationDelta, scale, position);

            utils::Instance& instance = m_Scene.instances[ m_AnimatedInstances[i].instanceID ];
            instance.rotation = transform;
            instance.position = ToDouble(position);
        }
    }

    // Adjust settings if tracing mode has been changed to / from "probabilistic sampling"
    if (m_Settings.RR && m_Settings.tracingMode == RESOLUTION_HALF)
        m_Settings.tracingMode = RESOLUTION_FULL_PROBABILISTIC;

    if (m_Settings.tracingMode != m_SettingsPrev.tracingMode && (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC || m_SettingsPrev.tracingMode == RESOLUTION_FULL_PROBABILISTIC))
    {
        nrd::ReblurSettings reblurDefaults = {};
        nrd::ReblurSettings relaxDefaults = {};

        if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
        {
            m_ReblurSettings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3;
            m_ReblurSettings.diffusePrepassBlurRadius = reblurDefaults.specularPrepassBlurRadius;
            m_ReblurSettings.specularPrepassBlurRadius = reblurDefaults.specularPrepassBlurRadius;

            m_RelaxSettings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::AREA_3X3;
            m_RelaxSettings.diffusePrepassBlurRadius = relaxDefaults.specularPrepassBlurRadius;
            m_RelaxSettings.specularPrepassBlurRadius = relaxDefaults.specularPrepassBlurRadius;
        }
        else
        {
            m_ReblurSettings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::OFF;
            m_ReblurSettings.diffusePrepassBlurRadius = reblurDefaults.diffusePrepassBlurRadius;
            m_ReblurSettings.specularPrepassBlurRadius = reblurDefaults.specularPrepassBlurRadius;

            m_RelaxSettings.hitDistanceReconstructionMode = nrd::HitDistanceReconstructionMode::OFF;
            m_RelaxSettings.diffusePrepassBlurRadius = relaxDefaults.diffusePrepassBlurRadius;
            m_RelaxSettings.specularPrepassBlurRadius = relaxDefaults.specularPrepassBlurRadius;
        }
    }

    // Print out information
    if (m_SettingsPrev.resolutionScale != m_Settings.resolutionScale ||
        m_SettingsPrev.tracingMode != m_Settings.tracingMode ||
        m_SettingsPrev.rpp != m_Settings.rpp ||
        frameIndex == 0)
    {
        std::array<uint32_t, 4> rppScale = {2, 1, 2, 2};
        std::array<float, 4> wScale = {1.0f, 1.0f, 0.5f, 0.5f};
        std::array<float, 4> hScale = {1.0f, 1.0f, 1.0f, 0.5f};

        uint32_t pw = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
        uint32_t ph = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);
        uint32_t iw = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale * wScale[m_Settings.tracingMode] + 0.5f);
        uint32_t ih = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale * hScale[m_Settings.tracingMode] + 0.5f);
        uint32_t rayNum = m_Settings.rpp * rppScale[m_Settings.tracingMode];
        float rpp = float( iw  * ih * rayNum ) / float( pw * ph );

        printf
        (
            "Output          : %ux%u\n"
            "  Primary rays  : %ux%u\n"
            "  Indirect rays : %ux%u x %u ray(s)\n"
            "  Indirect rpp  : %.2f\n",
            GetOutputResolution().x, GetOutputResolution().y,
            pw, ph,
            iw, ih, rayNum,
            rpp
        );
    }

    if (m_SettingsPrev.denoiser != m_Settings.denoiser || frameIndex == 0)
    {
        m_checkMeTests = nullptr;
        m_improveMeTests = nullptr;

        if (m_SceneFile.find("BistroInterior") != std::string::npos)
        {
            m_checkMeTests = &interior_checkMeTests;
            if (m_Settings.denoiser == DENOISER_REBLUR)
                m_improveMeTests = &REBLUR_interior_improveMeTests;
            else if (m_Settings.denoiser == DENOISER_RELAX)
                m_improveMeTests = &RELAX_interior_improveMeTests;
        }
    }

    // Global history reset
    float sunCurr = Smoothstep( -0.9f, 0.05f, Sin( DegToRad(m_Settings.sunElevation) ) );
    float sunPrev = Smoothstep( -0.9f, 0.05f, Sin( DegToRad(m_SettingsPrev.sunElevation) ) );
    float resetHistoryFactor = 1.0f - Smoothstep( 0.0f, 0.2f, Abs(sunCurr - sunPrev) );

    if (m_SettingsPrev.denoiser != m_Settings.denoiser)
        m_ForceHistoryReset = true;
    if (m_SettingsPrev.denoiser == DENOISER_REFERENCE && m_SettingsPrev.tracingMode != m_Settings.tracingMode)
        m_ForceHistoryReset = true;
    if (m_SettingsPrev.ortho != m_Settings.ortho)
        m_ForceHistoryReset = true;
    if (m_SettingsPrev.onScreen != m_Settings.onScreen)
        m_ForceHistoryReset = true;
    if (m_SettingsPrev.RR != m_Settings.RR)
        m_ForceHistoryReset = true;
    if (frameIndex == 0)
        m_ForceHistoryReset = true;

    // NRD common settings
    if (m_Settings.adaptiveAccumulation)
    {
        bool isFastHistoryEnabled = m_Settings.maxAccumulatedFrameNum > m_Settings.maxFastAccumulatedFrameNum;

        float fps = 1000.0f / m_Timer.GetSmoothedFrameTime();
        float maxAccumulatedFrameNum = Clamp(ACCUMULATION_TIME * fps, 5.0f, float(MAX_HISTORY_FRAME_NUM));
        float maxFastAccumulatedFrameNum = isFastHistoryEnabled ? (maxAccumulatedFrameNum / 5.0f) : float(MAX_HISTORY_FRAME_NUM);

        m_Settings.maxAccumulatedFrameNum = int32_t(maxAccumulatedFrameNum + 0.5f);
        m_Settings.maxFastAccumulatedFrameNum = int32_t(maxFastAccumulatedFrameNum + 0.5f);
    }

    uint32_t maxAccumulatedFrameNum = uint32_t(m_Settings.maxAccumulatedFrameNum * resetHistoryFactor + 0.5f);
    uint32_t maxFastAccumulatedFrameNum = uint32_t(m_Settings.maxFastAccumulatedFrameNum * resetHistoryFactor + 0.5f);

    m_ReblurSettings.maxAccumulatedFrameNum = maxAccumulatedFrameNum;
    m_ReblurSettings.maxFastAccumulatedFrameNum = maxFastAccumulatedFrameNum;
    m_ReblurSettings.checkerboardMode = m_Settings.tracingMode == RESOLUTION_HALF ? nrd::CheckerboardMode::WHITE : nrd::CheckerboardMode::OFF;
    m_ReblurSettings.enableMaterialTestForDiffuse = true;
    m_ReblurSettings.enableMaterialTestForSpecular = true;

    m_RelaxSettings.diffuseMaxAccumulatedFrameNum = maxAccumulatedFrameNum;
    m_RelaxSettings.diffuseMaxFastAccumulatedFrameNum = maxFastAccumulatedFrameNum;
    m_RelaxSettings.specularMaxAccumulatedFrameNum = maxAccumulatedFrameNum;
    m_RelaxSettings.specularMaxFastAccumulatedFrameNum = maxFastAccumulatedFrameNum;
    m_RelaxSettings.checkerboardMode = m_Settings.tracingMode == RESOLUTION_HALF ? nrd::CheckerboardMode::WHITE : nrd::CheckerboardMode::OFF;
    m_RelaxSettings.enableMaterialTestForDiffuse = true;
    m_RelaxSettings.enableMaterialTestForSpecular = true;

    bool wantPrintf = IsButtonPressed(Button::Middle) || IsKeyToggled(Key::P);

    uint32_t rectW = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
    uint32_t rectH = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);

    memcpy(m_CommonSettings.viewToClipMatrix, &m_Camera.state.mViewToClip, sizeof(m_Camera.state.mViewToClip));
    memcpy(m_CommonSettings.viewToClipMatrixPrev, &m_Camera.statePrev.mViewToClip, sizeof(m_Camera.statePrev.mViewToClip));
    memcpy(m_CommonSettings.worldToViewMatrix, &m_Camera.state.mWorldToView, sizeof(m_Camera.state.mWorldToView));
    memcpy(m_CommonSettings.worldToViewMatrixPrev, &m_Camera.statePrev.mWorldToView, sizeof(m_Camera.statePrev.mWorldToView));
    m_CommonSettings.motionVectorScale[0] = m_Settings.mvType == MV_3D ? 1.0f : 1.0f / float(rectW);
    m_CommonSettings.motionVectorScale[1] = m_Settings.mvType == MV_3D ? 1.0f : 1.0f / float(rectH);
    m_CommonSettings.motionVectorScale[2] = m_Settings.mvType != MV_2D ? 1.0f : 0.0f;
    m_CommonSettings.cameraJitter[0] = m_Settings.cameraJitter ? m_Camera.state.viewportJitter.x : 0.0f;
    m_CommonSettings.cameraJitter[1] = m_Settings.cameraJitter ? m_Camera.state.viewportJitter.y : 0.0f;
    m_CommonSettings.cameraJitterPrev[0] = m_Settings.cameraJitter ? m_Camera.statePrev.viewportJitter.x : 0.0f;
    m_CommonSettings.cameraJitterPrev[1] = m_Settings.cameraJitter ? m_Camera.statePrev.viewportJitter.y : 0.0f;
    m_CommonSettings.resourceSize[0] = (uint16_t)m_RenderResolution.x;
    m_CommonSettings.resourceSize[1] = (uint16_t)m_RenderResolution.y;
    m_CommonSettings.resourceSizePrev[0] = (uint16_t)m_RenderResolution.x;
    m_CommonSettings.resourceSizePrev[1] = (uint16_t)m_RenderResolution.y;
    m_CommonSettings.rectSize[0] = (uint16_t)(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
    m_CommonSettings.rectSize[1] = (uint16_t)(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);
    m_CommonSettings.rectSizePrev[0] = (uint16_t)(m_RenderResolution.x * m_SettingsPrev.resolutionScale + 0.5f);
    m_CommonSettings.rectSizePrev[1] = (uint16_t)(m_RenderResolution.y * m_SettingsPrev.resolutionScale + 0.5f);
    m_CommonSettings.denoisingRange = GetDenoisingRange();
    m_CommonSettings.disocclusionThreshold = m_Settings.disocclusionThreshold * 0.01f;
    m_CommonSettings.splitScreen = (m_Settings.denoiser == DENOISER_REFERENCE || m_Settings.RR) ? 1.0f : m_Settings.separator;
    m_CommonSettings.printfAt[0] = wantPrintf ? (uint16_t)ImGui::GetIO().MousePos.x : 9999;
    m_CommonSettings.printfAt[1] = wantPrintf ? (uint16_t)ImGui::GetIO().MousePos.y : 9999;
    m_CommonSettings.debug = m_Settings.debug;
    m_CommonSettings.frameIndex = frameIndex;
    m_CommonSettings.accumulationMode = m_ForceHistoryReset ? nrd::AccumulationMode::CLEAR_AND_RESTART : nrd::AccumulationMode::CONTINUE;
    m_CommonSettings.isMotionVectorInWorldSpace = m_Settings.mvType == MV_3D;
    m_CommonSettings.isBaseColorMetalnessAvailable = true;
    m_CommonSettings.enableValidation = m_ShowValidationOverlay;

    m_NRD.NewFrame();
    m_NRD.SetCommonSettings(m_CommonSettings);

    UpdateConstantBuffer(frameIndex, maxAccumulatedFrameNum);
    GatherInstanceData();

    NRI.CopyStreamerUpdateRequests(*m_Streamer);
}

void Sample::LoadScene()
{
    // Proxy geometry, which will be instancinated
    std::string sceneFile = utils::GetFullPath("ShaderBalls/ShaderBalls.gltf", utils::DataFolder::SCENES);
    NRI_ABORT_ON_FALSE( utils::LoadScene(sceneFile, m_Scene, !ALLOW_BLAS_MERGING) );

    m_ProxyInstancesNum = helper::GetCountOf(m_Scene.instances);

    // The scene
    sceneFile = utils::GetFullPath(m_SceneFile, utils::DataFolder::SCENES);
    NRI_ABORT_ON_FALSE( utils::LoadScene(sceneFile, m_Scene, !ALLOW_BLAS_MERGING) );

    // Some scene dependent settings
    m_ReblurSettings = GetDefaultReblurSettings();
    m_RelaxSettings = GetDefaultRelaxSettings();

    if (m_SceneFile.find("BistroInterior") != std::string::npos)
    {
        m_Settings.exposure = 80.0f;
        m_Settings.emission = true;
        m_Settings.animatedObjectScale = 0.5f;
        m_Settings.sunElevation = 7.0f;
    }
    else if (m_SceneFile.find("BistroExterior") != std::string::npos)
    {
        m_Settings.exposure = 50.0f;
        m_Settings.emission = true;
    }
    else if (m_SceneFile.find("Hair") != std::string::npos)
    {
        m_Settings.exposure = 2.0f;
        m_Settings.bounceNum = 4;
    }
    else if (m_SceneFile.find("ShaderBalls") != std::string::npos)
        m_Settings.exposure = 1.7f;
}

void Sample::AddInnerGlassSurfaces()
{
    // IMPORTANT: this is only valid for non-merged instances, when each instance represents a single object
    // TODO: try thickness emulation in TraceTransparent shader

    size_t instanceNum = m_Scene.instances.size();
    for (size_t i = 0; i < instanceNum; i++)
    {
        const utils::Instance& instance = m_Scene.instances[i];
        const utils::Material& material = m_Scene.materials[instance.materialIndex];

        // Skip non-transparent objects
        if (!material.IsTransparent())
            continue;

        const utils::MeshInstance &meshInstance = m_Scene.meshInstances[instance.meshInstanceIndex];
        const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];
        float3 size = mesh.aabb.vMax - mesh.aabb.vMin;
        size *= instance.rotation.GetScale();

        // Skip too thin objects
        float minSize = Min(size.x, Min(size.y, size.z));
        if (minSize < GLASS_THICKNESS * 2.0f)
            continue;

        // Skip objects, which look "merged"
        /*
        float maxSize = Max(size.x, Max(size.y, size.z));
        if (maxSize > 0.5f)
            continue;
        */

        utils::Instance innerInstance = instance;
        innerInstance.scale = (size - GLASS_THICKNESS) / (size + 1e-15f);

        m_Scene.instances.push_back(innerInstance);
    }
}

void Sample::GenerateAnimatedCubes()
{
    for (uint32_t i = 0; i < MAX_ANIMATED_INSTANCE_NUM; i++)
    {
        float3 position = Lerp(m_Scene.aabb.vMin, m_Scene.aabb.vMax, Rand::uf3(&m_FastRandState));

        AnimatedInstance animatedInstance = {};
        animatedInstance.instanceID = helper::GetCountOf(m_Scene.instances);
        animatedInstance.basePosition = position;
        animatedInstance.durationSec = Rand::uf1(&m_FastRandState) * 10.0f + 5.0f;
        animatedInstance.progressedSec = animatedInstance.durationSec * Rand::uf1(&m_FastRandState);
        animatedInstance.rotationAxis = Normalize( Rand::sf3(&m_FastRandState) );
        animatedInstance.elipseAxis = Rand::sf3(&m_FastRandState) * 5.0f;
        animatedInstance.reverseDirection = Rand::sf1(&m_FastRandState) < 0.0f;
        animatedInstance.reverseRotation = Rand::sf1(&m_FastRandState) < 0.0f;
        m_AnimatedInstances.push_back(animatedInstance);

        utils::Instance instance = m_Scene.instances[i % m_ProxyInstancesNum];
        instance.allowUpdate = true;

        m_Scene.instances.push_back(instance);
    }
}

nri::Format Sample::CreateSwapChain()
{
    nri::SwapChainDesc swapChainDesc = {};
    swapChainDesc.window = GetWindow();
    swapChainDesc.commandQueue = m_CommandQueue;
    swapChainDesc.format = nri::SwapChainFormat::BT709_G22_8BIT; // or BT709_G10_16BIT
    swapChainDesc.verticalSyncInterval = m_VsyncInterval;
    swapChainDesc.width = (uint16_t)GetWindowResolution().x;
    swapChainDesc.height = (uint16_t)GetWindowResolution().y;
    swapChainDesc.textureNum = SWAP_CHAIN_TEXTURE_NUM;

    NRI_ABORT_ON_FAILURE(NRI.CreateSwapChain(*m_Device, swapChainDesc, m_SwapChain));
    m_IsSrgb = swapChainDesc.format != nri::SwapChainFormat::BT709_G10_16BIT;

    uint32_t swapChainTextureNum = 0;
    nri::Texture* const* swapChainTextures = NRI.GetSwapChainTextures(*m_SwapChain, swapChainTextureNum);
    const nri::TextureDesc& swapChainTextureDesc = NRI.GetTextureDesc(*swapChainTextures[0]);
    nri::Format swapChainFormat = swapChainTextureDesc.format;

    for (uint32_t i = 0; i < swapChainTextureNum; i++)
    {
        m_SwapChainBuffers.emplace_back();
        BackBuffer& backBuffer = m_SwapChainBuffers.back();

        backBuffer = {};
        backBuffer.texture = swapChainTextures[i];

        char name[32];
        snprintf(name, sizeof(name), "Texture::SwapChain#%u", i);
        NRI.SetTextureDebugName(*backBuffer.texture, name);

        nri::Texture2DViewDesc textureViewDesc = {backBuffer.texture, nri::Texture2DViewType::COLOR_ATTACHMENT, swapChainFormat};
        NRI_ABORT_ON_FAILURE(NRI.CreateTexture2DView(textureViewDesc, backBuffer.colorAttachment));
    }

    return swapChainFormat;
}

void Sample::CreateCommandBuffers()
{
    for (Frame& frame : m_Frames)
    {
        NRI_ABORT_ON_FAILURE(NRI.CreateCommandAllocator(*m_CommandQueue, frame.commandAllocator));
        NRI_ABORT_ON_FAILURE(NRI.CreateCommandBuffer(*frame.commandAllocator, frame.commandBuffer));
    }
}

void Sample::CreatePipelineLayoutAndDescriptorPool()
{
    const nri::DescriptorRangeDesc descriptorRanges0[] =
    {
        { 0, 3, nri::DescriptorType::SAMPLER, nri::StageBits::COMPUTE_SHADER },
    };

    const nri::DescriptorRangeDesc descriptorRanges1[] =
    {
        { 0, 12, nri::DescriptorType::TEXTURE, nri::StageBits::COMPUTE_SHADER },
        { 0, 13, nri::DescriptorType::STORAGE_TEXTURE, nri::StageBits::COMPUTE_SHADER },
    };

    const uint32_t textureNum = helper::GetCountOf(m_Scene.materials) * TEXTURES_PER_MATERIAL;
    nri::DescriptorRangeDesc descriptorRanges2[] =
    {
        { 0, 2, nri::DescriptorType::ACCELERATION_STRUCTURE, nri::StageBits::COMPUTE_SHADER },
        { 2, 3, nri::DescriptorType::STRUCTURED_BUFFER, nri::StageBits::COMPUTE_SHADER },
        { 5, textureNum, nri::DescriptorType::TEXTURE, nri::StageBits::COMPUTE_SHADER, nri::VARIABLE_DESCRIPTOR_NUM, nri::DESCRIPTOR_ARRAY },
    };

    const nri::DescriptorRangeDesc descriptorRanges3[] =
    {
        { 0, 3, nri::DescriptorType::STRUCTURED_BUFFER, nri::StageBits::COMPUTE_SHADER },
        { 0, 2, nri::DescriptorType::STORAGE_STRUCTURED_BUFFER, nri::StageBits::COMPUTE_SHADER },
    };

   nri::DynamicConstantBufferDesc dynamicConstantBuffer = { 0, nri::StageBits::COMPUTE_SHADER };

    const nri::DescriptorSetDesc descriptorSetDesc[] =
    {
        { 0, descriptorRanges0, helper::GetCountOf(descriptorRanges0), &dynamicConstantBuffer, 1 },
        { 1, descriptorRanges1, helper::GetCountOf(descriptorRanges1), nullptr, 0, nri::PARTIALLY_BOUND },
        { 2, descriptorRanges2, helper::GetCountOf(descriptorRanges2) },
        { 3, descriptorRanges3, helper::GetCountOf(descriptorRanges3), &dynamicConstantBuffer, 1, nri::PARTIALLY_BOUND },
    };

    nri::PipelineLayoutDesc pipelineLayoutDesc = {};
    pipelineLayoutDesc.descriptorSets = descriptorSetDesc;
    pipelineLayoutDesc.descriptorSetNum = helper::GetCountOf(descriptorSetDesc);
    pipelineLayoutDesc.shaderStages = nri::StageBits::COMPUTE_SHADER;

    NRI_ABORT_ON_FAILURE(NRI.CreatePipelineLayout(*m_Device, pipelineLayoutDesc, m_PipelineLayout));

    nri::DescriptorPoolDesc descriptorPoolDesc = {};
    {
        descriptorPoolDesc.descriptorSetMaxNum += 1;
        descriptorPoolDesc.dynamicConstantBufferMaxNum += descriptorSetDesc[0].dynamicConstantBufferNum;
        descriptorPoolDesc.samplerMaxNum += descriptorSetDesc[0].ranges[0].descriptorNum * BUFFERED_FRAME_MAX_NUM;

        descriptorPoolDesc.descriptorSetMaxNum += uint32_t(DescriptorSet::MAX_NUM);
        descriptorPoolDesc.textureMaxNum += descriptorSetDesc[1].ranges[0].descriptorNum * uint32_t(DescriptorSet::MAX_NUM);
        descriptorPoolDesc.storageTextureMaxNum += descriptorSetDesc[1].ranges[1].descriptorNum * uint32_t(DescriptorSet::MAX_NUM);

        descriptorPoolDesc.descriptorSetMaxNum += 1;
        descriptorPoolDesc.accelerationStructureMaxNum += descriptorSetDesc[2].ranges[0].descriptorNum;
        descriptorPoolDesc.structuredBufferMaxNum += descriptorSetDesc[2].ranges[1].descriptorNum;
        descriptorPoolDesc.textureMaxNum += descriptorSetDesc[2].ranges[2].descriptorNum;

        descriptorPoolDesc.descriptorSetMaxNum += 2;
        descriptorPoolDesc.dynamicConstantBufferMaxNum += descriptorSetDesc[3].dynamicConstantBufferNum * 2;
        descriptorPoolDesc.structuredBufferMaxNum += descriptorSetDesc[3].ranges[0].descriptorNum * 2;
        descriptorPoolDesc.storageStructuredBufferMaxNum += descriptorSetDesc[3].ranges[1].descriptorNum * 2;
    }

    NRI_ABORT_ON_FAILURE(NRI.CreateDescriptorPool(*m_Device, descriptorPoolDesc, m_DescriptorPool));
}

void Sample::CreatePipelines()
{
    if (!m_Pipelines.empty())
    {
        NRI.WaitForIdle(*m_CommandQueue);

        for (uint32_t i = 0; i < m_Pipelines.size(); i++)
            NRI.DestroyPipeline(*m_Pipelines[i]);
        m_Pipelines.clear();

        m_NRD.CreatePipelines();
    }

    utils::ShaderCodeStorage shaderCodeStorage;

    nri::ComputePipelineDesc pipelineDesc = {};
    pipelineDesc.pipelineLayout = m_PipelineLayout;

    nri::Pipeline* pipeline = nullptr;
    const nri::DeviceDesc& deviceDesc = NRI.GetDeviceDesc(*m_Device);

    { // Pipeline::MorphMeshUpdateVertices
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "MorphMeshUpdateVertices.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::MorphMeshUpdatePrimitives
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "MorphMeshUpdatePrimitives.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::TraceAmbient
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "TraceAmbient.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::TraceOpaque
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "TraceOpaque.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::Composition
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "Composition.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::TraceTransparent
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "TraceTransparent.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::Temporal
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "Temporal.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::Upsample
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "Upsample.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::UpsampleNis
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "UpsampleNis.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::DlssBefore
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "DlssBefore.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }

    { // Pipeline::DlssAfter
        pipelineDesc.shader = utils::LoadShader(deviceDesc.graphicsAPI, "DlssAfter.cs", shaderCodeStorage);

        NRI_ABORT_ON_FAILURE(NRI.CreateComputePipeline(*m_Device, pipelineDesc, pipeline));
        m_Pipelines.push_back(pipeline);
    }
}

void Sample::CreateAccelerationStructures()
{
    double stamp1 = m_Timer.GetTimeStamp();

    struct Parameters
    {
        nri::AccelerationStructure* accelerationStructure;
        uint64_t scratchOffset;
        uint32_t geometryObjectBase;
        uint32_t geometryObjectsNum;
        nri::AccelerationStructureBuildBits buildBits;
    };

    uint64_t primitivesNum = 0;
    std::vector<Parameters> parameters;
    std::vector<nri::GeometryObject> geometryObjects;

    geometryObjects.reserve(m_Scene.instances.size()); // reallocation is NOT allowed!

    // Calculate temp memory size
    std::vector<uint32_t> dynamicMeshInstances;
    uint64_t uploadSize = 0;
    uint64_t geometryOffset = 0;

    for (size_t i = m_ProxyInstancesNum; i < m_Scene.instances.size(); i++)
    {
        const utils::Instance& instance = m_Scene.instances[i];
        const utils::Material& material = m_Scene.materials[instance.materialIndex];

        if (material.IsOff())
            continue;

        if (instance.allowUpdate)
        {
            if (std::find(dynamicMeshInstances.begin(), dynamicMeshInstances.end(), instance.meshInstanceIndex) != dynamicMeshInstances.end())
                continue;
            else
                dynamicMeshInstances.push_back(instance.meshInstanceIndex);
        }

        const utils::MeshInstance& meshInstance = m_Scene.meshInstances[instance.meshInstanceIndex];
        const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

        uint64_t vertexDataSize = mesh.vertexNum * sizeof(float[3]);
        uint64_t indexDataSize = helper::Align(mesh.indexNum * sizeof(utils::Index), 4);
        uint64_t transformDataSize = instance.allowUpdate ? 0 : sizeof(float[12]);

        if (material.IsEmissive())
        {
            // Emissive meshes apper twice: in BLAS_StaticOpaque and in BLAS_StaticEmissive
            vertexDataSize *= 2;
            indexDataSize *= 2;
            transformDataSize *= 2;
        }

        uploadSize += vertexDataSize + indexDataSize + transformDataSize;
        geometryOffset += transformDataSize;
    }

    // Create temp buffer in UPLOAD heap
    nri::Buffer* uploadBuffer = nullptr;
    nri::Memory* uploadMemory = nullptr;
    {
        const nri::BufferDesc bufferDesc = {uploadSize, 0, nri::BufferUsageBits::ACCELERATION_STRUCTURE_BUILD_READ};
        NRI_ABORT_ON_FAILURE(NRI.CreateBuffer(*m_Device, bufferDesc, uploadBuffer));

        nri::MemoryDesc memoryDesc = {};
        NRI.GetBufferMemoryInfo(*uploadBuffer, nri::MemoryLocation::HOST_UPLOAD, memoryDesc);

        NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, uploadMemory));

        const nri::BufferMemoryBindingDesc memoryBindingDesc = {uploadMemory, uploadBuffer};
        NRI_ABORT_ON_FAILURE(NRI.BindBufferMemory(*m_Device, &memoryBindingDesc, 1));
    }

    uint8_t* uploadData = (uint8_t*)NRI.MapBuffer(*uploadBuffer, 0, nri::WHOLE_SIZE);

    { // AccelerationStructure::TLAS_World
        nri::AccelerationStructureDesc accelerationStructureDesc = {};
        accelerationStructureDesc.type = nri::AccelerationStructureType::TOP_LEVEL;
        accelerationStructureDesc.flags = TLAS_BUILD_BITS;
        accelerationStructureDesc.instanceOrGeometryObjectNum = helper::GetCountOf(m_Scene.instances);

        nri::AccelerationStructure* accelerationStructure = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.CreateAccelerationStructure(*m_Device, accelerationStructureDesc, accelerationStructure));
        m_AccelerationStructures.push_back(accelerationStructure);

        nri::MemoryDesc memoryDesc = {};
        NRI.GetAccelerationStructureMemoryInfo(*accelerationStructure, memoryDesc);

        nri::Memory* memory = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, memory));
        m_MemoryAllocations.push_back(memory);

        const nri::AccelerationStructureMemoryBindingDesc memoryBindingDesc = {memory, accelerationStructure};
        NRI_ABORT_ON_FAILURE(NRI.BindAccelerationStructureMemory(*m_Device, &memoryBindingDesc, 1));

        // Descriptor::World_AccelerationStructure
        nri::Descriptor* descriptor = nullptr;
        NRI.CreateAccelerationStructureDescriptor(*accelerationStructure, descriptor);
        m_Descriptors.push_back(descriptor);
    }

    { // AccelerationStructure::TLAS_Emissive
        nri::AccelerationStructureDesc accelerationStructureDesc = {};
        accelerationStructureDesc.type = nri::AccelerationStructureType::TOP_LEVEL;
        accelerationStructureDesc.flags = TLAS_BUILD_BITS;
        accelerationStructureDesc.instanceOrGeometryObjectNum = helper::GetCountOf(m_Scene.instances);

        nri::AccelerationStructure* accelerationStructure = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.CreateAccelerationStructure(*m_Device, accelerationStructureDesc, accelerationStructure));
        m_AccelerationStructures.push_back(accelerationStructure);

        nri::MemoryDesc memoryDesc = {};
        NRI.GetAccelerationStructureMemoryInfo(*accelerationStructure, memoryDesc);

        nri::Memory* memory = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, memory));
        m_MemoryAllocations.push_back(memory);

        const nri::AccelerationStructureMemoryBindingDesc memoryBindingDesc = {memory, accelerationStructure};
        NRI_ABORT_ON_FAILURE(NRI.BindAccelerationStructureMemory(*m_Device, &memoryBindingDesc, 1));

        // Descriptor::Light_AccelerationStructure
        nri::Descriptor* descriptor = nullptr;
        NRI.CreateAccelerationStructureDescriptor(*accelerationStructure, descriptor);
        m_Descriptors.push_back(descriptor);
    }

    // Create BOTTOM_LEVEL acceleration structures for static geometry
    uint64_t scratchSize = 0;

    for (uint32_t mode = (uint32_t)AccelerationStructure::BLAS_StaticOpaque; mode <= (uint32_t)AccelerationStructure::BLAS_StaticEmissive; mode++)
    {
        size_t geometryObjectBase = geometryObjects.size();

        for (size_t i = m_ProxyInstancesNum; i < m_Scene.instances.size(); i++)
        {
            const utils::Instance& instance = m_Scene.instances[i];
            const utils::Material& material = m_Scene.materials[instance.materialIndex];

            if (material.IsOff())
                continue;

            if (instance.allowUpdate)
                continue;

            if (mode == (uint32_t)AccelerationStructure::BLAS_StaticOpaque)
            {
                if (material.IsTransparent())
                    continue;

                m_OpaqueObjectsNum++;
            }
            else if (mode == (uint32_t)AccelerationStructure::BLAS_StaticTransparent)
            {
                if (!material.IsTransparent())
                    continue;

                m_TransparentObjectsNum++;
            }
            else if (mode == (uint32_t)AccelerationStructure::BLAS_StaticEmissive)
            {
                if (!material.IsEmissive())
                    continue;

                m_EmissiveObjectsNum++;
            }

            const utils::MeshInstance& meshInstance = m_Scene.meshInstances[instance.meshInstanceIndex];
            const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

            // Copy geometry to temp buffer
            uint64_t vertexDataSize = mesh.vertexNum * sizeof(float[3]);
            uint8_t* p = uploadData + geometryOffset;
            for (uint32_t v = 0; v < mesh.vertexNum; v++)
            {
                memcpy(p, m_Scene.vertices[mesh.vertexOffset + v].position, sizeof(float[3]));
                p += sizeof(float[3]);
            }

            uint64_t indexDataSize = mesh.indexNum * sizeof(utils::Index);
            memcpy(p, &m_Scene.indices[mesh.indexOffset], indexDataSize);

            // Copy transform to temp buffer
            float4x4 mObjectToWorld = instance.rotation;
            if (instance.scale != float3(1.0f))
            {
                float4x4 translation;
                translation.SetupByTranslation( ToFloat(instance.position) - mesh.aabb.GetCenter() );

                float4x4 translationInv = translation;
                translationInv.InvertOrtho();

                float4x4 scale;
                scale.SetupByScale(instance.scale);

                mObjectToWorld = mObjectToWorld * translationInv * scale * translation;
            }
            mObjectToWorld.AddTranslation( ToFloat(instance.position) );

            mObjectToWorld.Transpose3x4();

            uint64_t transformOffset = geometryObjects.size() * sizeof(float[12]);
            memcpy(uploadData + transformOffset, mObjectToWorld.a16, sizeof(float[12]));

            // Add geometry object
            nri::GeometryObject& geometryObject = geometryObjects.emplace_back();
            geometryObject = {};
            geometryObject.type = nri::GeometryType::TRIANGLES;
            geometryObject.flags = material.IsAlphaOpaque() ? nri::BottomLevelGeometryBits::NONE : nri::BottomLevelGeometryBits::OPAQUE_GEOMETRY;
            geometryObject.triangles.vertexBuffer = uploadBuffer;
            geometryObject.triangles.vertexOffset = geometryOffset;
            geometryObject.triangles.vertexNum = mesh.vertexNum;
            geometryObject.triangles.vertexStride = sizeof(float[3]);
            geometryObject.triangles.vertexFormat = nri::Format::RGB32_SFLOAT;
            geometryObject.triangles.indexBuffer = uploadBuffer;
            geometryObject.triangles.indexOffset = geometryOffset + vertexDataSize;
            geometryObject.triangles.indexNum = mesh.indexNum;
            geometryObject.triangles.indexType = sizeof(utils::Index) == 2 ? nri::IndexType::UINT16 : nri::IndexType::UINT32;
            geometryObject.triangles.transformBuffer = uploadBuffer;
            geometryObject.triangles.transformOffset = transformOffset;

            // Update geometry offset
            geometryOffset += vertexDataSize + helper::Align(indexDataSize, 4);
            primitivesNum += mesh.indexNum / 3;
        }

        uint32_t geometryObjectsNum = (uint32_t)(geometryObjects.size() - geometryObjectBase);
        if (geometryObjectsNum)
        {
            // Create BLAS
            nri::AccelerationStructureDesc accelerationStructureDesc = {};
            accelerationStructureDesc.type = nri::AccelerationStructureType::BOTTOM_LEVEL;
            accelerationStructureDesc.flags = BLAS_RIGID_MESH_BUILD_BITS;
            accelerationStructureDesc.instanceOrGeometryObjectNum = geometryObjectsNum;
            accelerationStructureDesc.geometryObjects = &geometryObjects[geometryObjectBase];

            nri::AccelerationStructure* accelerationStructure = nullptr;
            NRI_ABORT_ON_FAILURE(NRI.CreateAccelerationStructure(*m_Device, accelerationStructureDesc, accelerationStructure));
            m_AccelerationStructures.push_back(accelerationStructure);

            nri::MemoryDesc memoryDesc = {};
            NRI.GetAccelerationStructureMemoryInfo(*accelerationStructure, memoryDesc);

            nri::Memory* memory = nullptr;
            NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, memory));
            m_MemoryAllocations.push_back(memory);

            const nri::AccelerationStructureMemoryBindingDesc memoryBindingDesc = {memory, accelerationStructure};
            NRI_ABORT_ON_FAILURE(NRI.BindAccelerationStructureMemory(*m_Device, &memoryBindingDesc, 1));

            // Update parameters
            parameters.push_back( {accelerationStructure, scratchSize, (uint32_t)geometryObjectBase, geometryObjectsNum, accelerationStructureDesc.flags} );

            uint64_t size = NRI.GetAccelerationStructureBuildScratchBufferSize(*accelerationStructure);
            scratchSize += helper::Align(size, 256);
        }
        else
        {
            // Needed only to preserve order
            m_AccelerationStructures.push_back(nullptr);
            m_MemoryAllocations.push_back(nullptr);
        }
    }

    // Create BOTTOM_LEVEL acceleration structures for dynamic geometry
    for (uint32_t dynamicMeshInstanceIndex : dynamicMeshInstances)
    {
        utils::MeshInstance& meshInstance = m_Scene.meshInstances[dynamicMeshInstanceIndex];
        const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

        meshInstance.blasIndex = (uint32_t)m_AccelerationStructures.size();

        // Copy geometry to temp buffer
        uint64_t vertexDataSize = mesh.vertexNum * sizeof(float[3]);
        uint8_t* p = uploadData + geometryOffset;
        for (uint32_t v = 0; v < mesh.vertexNum; v++)
        {
            memcpy(p, m_Scene.vertices[mesh.vertexOffset + v].position, sizeof(float[3]));
            p += sizeof(float[3]);
        }

        uint64_t indexDataSize = mesh.indexNum * sizeof(utils::Index);
        memcpy(p, &m_Scene.indices[mesh.indexOffset], indexDataSize);

        // Add geometry object
        nri::GeometryObject& geometryObject = geometryObjects.emplace_back();
        geometryObject = {};
        geometryObject.type = nri::GeometryType::TRIANGLES;
        geometryObject.flags = nri::BottomLevelGeometryBits::NONE; // will be set in TLAS instance
        geometryObject.triangles.vertexBuffer = uploadBuffer;
        geometryObject.triangles.vertexOffset = geometryOffset;
        geometryObject.triangles.vertexNum = mesh.vertexNum;
        geometryObject.triangles.vertexStride = sizeof(float[3]);
        geometryObject.triangles.vertexFormat = nri::Format::RGB32_SFLOAT;
        geometryObject.triangles.indexBuffer = uploadBuffer;
        geometryObject.triangles.indexOffset = geometryOffset + vertexDataSize;
        geometryObject.triangles.indexNum = mesh.indexNum;
        geometryObject.triangles.indexType = sizeof(utils::Index) == 2 ? nri::IndexType::UINT16 : nri::IndexType::UINT32;

        // Create BLAS
        nri::AccelerationStructureDesc accelerationStructureDesc = {};
        accelerationStructureDesc.type = nri::AccelerationStructureType::BOTTOM_LEVEL;
        accelerationStructureDesc.flags = mesh.HasMorphTargets() ? BLAS_DEFORMABLE_MESH_BUILD_BITS : BLAS_RIGID_MESH_BUILD_BITS;
        accelerationStructureDesc.instanceOrGeometryObjectNum = 1;
        accelerationStructureDesc.geometryObjects = &geometryObject;

        nri::AccelerationStructure* accelerationStructure = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.CreateAccelerationStructure(*m_Device, accelerationStructureDesc, accelerationStructure));
        m_AccelerationStructures.push_back(accelerationStructure);

        nri::MemoryDesc memoryDesc = {};
        NRI.GetAccelerationStructureMemoryInfo(*accelerationStructure, memoryDesc);

        nri::Memory* memory = nullptr;
        NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, memory));
        m_MemoryAllocations.push_back(memory);

        const nri::AccelerationStructureMemoryBindingDesc memoryBindingDesc = {memory, accelerationStructure};
        NRI_ABORT_ON_FAILURE(NRI.BindAccelerationStructureMemory(*m_Device, &memoryBindingDesc, 1));

        // Update parameters
        parameters.push_back( {accelerationStructure, scratchSize, (uint32_t)(geometryObjects.size() - 1), 1, accelerationStructureDesc.flags } );

        uint64_t size = NRI.GetAccelerationStructureBuildScratchBufferSize(*accelerationStructure);
        scratchSize += helper::Align(size, 256);

        if (mesh.HasMorphTargets())
            m_MorphMeshScratchSize += helper::Align(size, 256);

        // Update geometry offset
        geometryOffset += vertexDataSize + helper::Align(indexDataSize, 4);
        primitivesNum += mesh.indexNum / 3;
    }

    // Allocate scratch memory
    const nri::BufferDesc bufferDesc = {scratchSize, 0, nri::BufferUsageBits::RAY_TRACING_BUFFER | nri::BufferUsageBits::SHADER_RESOURCE_STORAGE};
    nri::Buffer* scratchBuffer = nullptr;
    NRI_ABORT_ON_FAILURE(NRI.CreateBuffer(*m_Device, bufferDesc, scratchBuffer));

    nri::MemoryDesc memoryDesc = {};
    NRI.GetBufferMemoryInfo(*scratchBuffer, nri::MemoryLocation::DEVICE, memoryDesc);

    nri::Memory* scratchMemory = nullptr;
    NRI_ABORT_ON_FAILURE(NRI.AllocateMemory(*m_Device, memoryDesc.type, memoryDesc.size, scratchMemory));

    const nri::BufferMemoryBindingDesc bufferMemoryBindingDesc = { scratchMemory, scratchBuffer };
    NRI_ABORT_ON_FAILURE(NRI.BindBufferMemory(*m_Device, &bufferMemoryBindingDesc, 1));

    // Create command allocator and command buffer
    nri::CommandAllocator* commandAllocator = nullptr;
    NRI.CreateCommandAllocator(*m_CommandQueue, commandAllocator);

    nri::CommandBuffer* commandBuffer = nullptr;
    NRI.CreateCommandBuffer(*commandAllocator, commandBuffer);

    double stamp2 = m_Timer.GetTimeStamp();

    // Record
    NRI.BeginCommandBuffer(*commandBuffer, nullptr);
    {
        for (const Parameters& params : parameters)
            NRI.CmdBuildBottomLevelAccelerationStructure(*commandBuffer, params.geometryObjectsNum, &geometryObjects[params.geometryObjectBase], params.buildBits, *params.accelerationStructure, *scratchBuffer, params.scratchOffset);
    }
    NRI.EndCommandBuffer(*commandBuffer);

    // Submit
    nri::QueueSubmitDesc queueSubmitDesc = {};
    queueSubmitDesc.commandBuffers = &commandBuffer;
    queueSubmitDesc.commandBufferNum = 1;

    NRI.QueueSubmit(*m_CommandQueue, queueSubmitDesc);

    // Wait idle
    NRI.WaitForIdle(*m_CommandQueue);

    double buildTime = m_Timer.GetTimeStamp() - stamp2;

    // Cleanup
    NRI.UnmapBuffer(*uploadBuffer);

    NRI.DestroyBuffer(*scratchBuffer);
    NRI.FreeMemory(*scratchMemory);

    NRI.DestroyBuffer(*uploadBuffer);
    NRI.FreeMemory(*uploadMemory);

    NRI.DestroyCommandBuffer(*commandBuffer);
    NRI.DestroyCommandAllocator(*commandAllocator);

    double totalTime = m_Timer.GetTimeStamp() - stamp1;

    printf(
        "Scene stats:\n"
        "  Instances     : %zu\n"
        "  Meshes        : %zu\n"
        "  Vertices      : %zu\n"
        "  Primitives    : %zu\n"
        "BVH stats:\n"
        "  Total time    : %.2f ms\n"
        "  Building time : %.2f ms\n"
        "  Scratch size  : %.2f Mb\n"
        "  BLAS num      : %zu\n"
        "  Geometries    : %zu\n"
        "  Primitives    : %zu\n"
        , m_Scene.instances.size()
        , m_Scene.meshes.size()
        , m_Scene.primitives.size()
        , m_Scene.vertices.size()
        , totalTime
        , buildTime
        , scratchSize / (1024.0 * 1024.0)
        , m_AccelerationStructures.size() - (size_t)AccelerationStructure::BLAS_StaticOpaque
        , geometryObjects.size()
        , primitivesNum
   );
}

void Sample::CreateSamplers()
{
    nri::Descriptor* descriptor = nullptr;

    { // Descriptor::LinearMipmapLinear_Sampler
        nri::SamplerDesc samplerDesc = {};
        samplerDesc.addressModes = {nri::AddressMode::REPEAT, nri::AddressMode::REPEAT};
        samplerDesc.filters = {nri::Filter::LINEAR, nri::Filter::LINEAR, nri::Filter::LINEAR};
        samplerDesc.mipMax = 16.0f;

        NRI_ABORT_ON_FAILURE( NRI.CreateSampler(*m_Device, samplerDesc, descriptor) );
        m_Descriptors.push_back(descriptor);
    }

    { // Descriptor::LinearMipmapNearest_Sampler
        nri::SamplerDesc samplerDesc = {};
        samplerDesc.addressModes = {nri::AddressMode::REPEAT, nri::AddressMode::REPEAT};
        samplerDesc.filters = {nri::Filter::LINEAR, nri::Filter::LINEAR, nri::Filter::NEAREST};
        samplerDesc.mipMax = 16.0f;

        NRI_ABORT_ON_FAILURE( NRI.CreateSampler(*m_Device, samplerDesc, descriptor) );
        m_Descriptors.push_back(descriptor);
    }

    { // Descriptor::NearestMipmapNearest_Sampler
        nri::SamplerDesc samplerDesc = {};
        samplerDesc.addressModes = {nri::AddressMode::REPEAT, nri::AddressMode::REPEAT};
        samplerDesc.filters = {nri::Filter::NEAREST, nri::Filter::NEAREST, nri::Filter::NEAREST};
        samplerDesc.mipMax = 16.0f;

        NRI_ABORT_ON_FAILURE( NRI.CreateSampler(*m_Device, samplerDesc, descriptor) );
        m_Descriptors.push_back(descriptor);
    }
}

inline nri::Format ConvertFormatToTextureStorageCompatible(nri::Format format)
{
    switch (format)
    {
        case nri::Format::D16_UNORM:                return nri::Format::R16_UNORM;
        case nri::Format::D24_UNORM_S8_UINT:        return nri::Format::R24_UNORM_X8;
        case nri::Format::D32_SFLOAT:               return nri::Format::R32_SFLOAT;
        case nri::Format::D32_SFLOAT_S8_UINT_X24:   return nri::Format::R32_SFLOAT_X8_X24;
        case nri::Format::RGBA8_SRGB:               return nri::Format::RGBA8_UNORM;
        case nri::Format::BGRA8_SRGB:               return nri::Format::BGRA8_UNORM;
        default:                                    return format;
    }
}

void Sample::CreateResources(nri::Format swapChainFormat)
{
    // TODO: DLSS doesn't support R16 UNORM/SNORM
#if( NRD_MODE == OCCLUSION )
    nri::Format dataFormat = m_DlssQuality != -1 ? nri::Format::R16_SFLOAT : nri::Format::R16_UNORM;
#elif( NRD_MODE == DIRECTIONAL_OCCLUSION )
    nri::Format dataFormat = m_DlssQuality != -1 ? nri::Format::RGBA16_SFLOAT : nri::Format::RGBA16_SNORM;
#else
    nri::Format dataFormat = nri::Format::RGBA16_SFLOAT;
#endif

#if( NRD_NORMAL_ENCODING == 0 )
    nri::Format normalFormat = nri::Format::RGBA8_UNORM;
#elif( NRD_NORMAL_ENCODING == 1 )
    nri::Format normalFormat = nri::Format::RGBA8_SNORM;
#elif( NRD_NORMAL_ENCODING == 2 )
    nri::Format normalFormat = nri::Format::R10_G10_B10_A2_UNORM;
#elif( NRD_NORMAL_ENCODING == 3 )
    nri::Format normalFormat = nri::Format::RGBA16_UNORM;
#elif( NRD_NORMAL_ENCODING == 4 )
    nri::Format normalFormat = nri::Format::RGBA16_SNORM;
#endif

    nri::Format taaFormat = m_IsSrgb ? nri::Format::R10_G10_B10_A2_UNORM : nri::Format::RGBA16_SFLOAT;

    const uint16_t w = (uint16_t)m_RenderResolution.x;
    const uint16_t h = (uint16_t)m_RenderResolution.y;
    const uint64_t instanceNum = m_Scene.instances.size() + MAX_ANIMATED_INSTANCE_NUM;
    const uint64_t instanceDataSize = instanceNum * sizeof(InstanceData);
    const uint64_t worldScratchBufferSize = NRI.GetAccelerationStructureBuildScratchBufferSize(*Get(AccelerationStructure::TLAS_World));
    const uint64_t lightScratchBufferSize = NRI.GetAccelerationStructureBuildScratchBufferSize(*Get(AccelerationStructure::TLAS_Emissive));

    std::vector<DescriptorDesc> descriptorDescs;

    m_InstanceData.resize(instanceNum);
    m_WorldTlasData.resize(instanceNum);
    m_LightTlasData.resize(instanceNum);

    // Buffers (DEVICE, read-only)
    CreateBuffer(descriptorDescs, "Buffer::InstanceData", instanceDataSize / sizeof(InstanceData), sizeof(InstanceData), nri::BufferUsageBits::SHADER_RESOURCE, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::MorphMeshIndices", m_Scene.morphMeshTotalIndicesNum, sizeof(utils::Index), nri::BufferUsageBits::SHADER_RESOURCE | nri::BufferUsageBits::ACCELERATION_STRUCTURE_BUILD_READ, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::MorphMeshVertices", m_Scene.morphVertices.size(), sizeof(utils::MorphVertex), nri::BufferUsageBits::SHADER_RESOURCE, nri::Format::UNKNOWN);

    // Buffers (DEVICE)
    CreateBuffer(descriptorDescs, "Buffer::MorphedPositions", m_Scene.morphedVerticesNum * MAX_ANIMATION_HISTORY_FRAME_NUM, sizeof(float4), nri::BufferUsageBits::SHADER_RESOURCE | nri::BufferUsageBits::SHADER_RESOURCE_STORAGE | nri::BufferUsageBits::ACCELERATION_STRUCTURE_BUILD_READ, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::MorphedAttributes", m_Scene.morphedVerticesNum, sizeof(MorphedAttributes), nri::BufferUsageBits::SHADER_RESOURCE | nri::BufferUsageBits::SHADER_RESOURCE_STORAGE, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::MorphedPrimitivePrevData", m_Scene.morphedPrimitivesNum, sizeof(MorphedPrimitivePrevData), nri::BufferUsageBits::SHADER_RESOURCE | nri::BufferUsageBits::SHADER_RESOURCE_STORAGE, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::PrimitiveData", m_Scene.totalInstancedPrimitivesNum, sizeof(PrimitiveData), nri::BufferUsageBits::SHADER_RESOURCE | nri::BufferUsageBits::SHADER_RESOURCE_STORAGE, nri::Format::UNKNOWN);
    CreateBuffer(descriptorDescs, "Buffer::WorldScratch", worldScratchBufferSize, 1, nri::BufferUsageBits::RAY_TRACING_BUFFER);
    CreateBuffer(descriptorDescs, "Buffer::LightScratch", lightScratchBufferSize, 1, nri::BufferUsageBits::RAY_TRACING_BUFFER);
    CreateBuffer(descriptorDescs, "Buffer::MorphMeshScratch", m_MorphMeshScratchSize, 1, nri::BufferUsageBits::RAY_TRACING_BUFFER);

    // Textures (DEVICE)
    CreateTexture(descriptorDescs, "Texture::Ambient", nri::Format::RGBA16_SFLOAT, 2, 2, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);
    CreateTexture(descriptorDescs, "Texture::ViewZ", nri::Format::R32_SFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Motion", nri::Format::RGBA16_SFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Normal_Roughness", normalFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::PsrThroughput", nri::Format::R10_G10_B10_A2_UNORM, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::BaseColor_Metalness", nri::Format::RGBA8_SRGB, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::DirectLighting", nri::Format::R11_G11_B10_UFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::DirectEmission", nri::Format::R11_G11_B10_UFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Shadow", nri::Format::RGBA8_UNORM, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Diff", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Spec", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Unfiltered_ShadowData", nri::Format::RG16_SFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Unfiltered_Diff", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Unfiltered_Spec", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Unfiltered_Shadow_Translucency", nri::Format::RGBA8_UNORM, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Validation", nri::Format::RGBA8_UNORM, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Composed_ViewZ", nri::Format::RGBA16_SFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);
    CreateTexture(descriptorDescs, "Texture::DlssOutput", nri::Format::R11_G11_B10_UFLOAT, (uint16_t)GetOutputResolution().x, (uint16_t)GetOutputResolution().y, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);
    CreateTexture(descriptorDescs, "Texture::Final", swapChainFormat, (uint16_t)GetWindowResolution().x, (uint16_t)GetWindowResolution().y, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::COPY_SOURCE);

    CreateTexture(descriptorDescs, "Texture::ComposedDiff", nri::Format::R11_G11_B10_UFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);
    CreateTexture(descriptorDescs, "Texture::ComposedSpec_ViewZ", nri::Format::RGBA16_SFLOAT, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);
    CreateTexture(descriptorDescs, "Texture::TaaHistory", taaFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::TaaHistoryPrev", taaFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE_STORAGE);

#if( NRD_MODE == SH )
    CreateTexture(descriptorDescs, "Texture::Unfiltered_DiffSh", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::Unfiltered_SpecSh", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::DiffSh", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
    CreateTexture(descriptorDescs, "Texture::SpecSh", dataFormat, w, h, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE | nri::TextureUsageBits::SHADER_RESOURCE_STORAGE, nri::AccessBits::SHADER_RESOURCE);
#endif

    CreateTexture(descriptorDescs, "Texture::NisData1", nri::Format::RGBA16_SFLOAT, kFilterSize / 4, kPhaseCount, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE, nri::AccessBits::UNKNOWN);
    CreateTexture(descriptorDescs, "Texture::NisData2", nri::Format::RGBA16_SFLOAT, kFilterSize / 4, kPhaseCount, 1, 1,
        nri::TextureUsageBits::SHADER_RESOURCE, nri::AccessBits::UNKNOWN);

    for (const utils::Texture* texture : m_Scene.textures)
        CreateTexture(descriptorDescs, "", texture->GetFormat(), texture->GetWidth(), texture->GetHeight(), texture->GetMipNum(), texture->GetArraySize(), nri::TextureUsageBits::SHADER_RESOURCE, nri::AccessBits::UNKNOWN);

    // Bind memory
    nri::ResourceGroupDesc resourceGroupDesc = {};
    resourceGroupDesc.memoryLocation = nri::MemoryLocation::DEVICE;
    resourceGroupDesc.bufferNum = helper::GetCountOf(m_Buffers);
    resourceGroupDesc.buffers = m_Buffers.data();
    resourceGroupDesc.textureNum = helper::GetCountOf(m_Textures);
    resourceGroupDesc.textures = m_Textures.data();

    size_t baseAllocation = m_MemoryAllocations.size();
    m_MemoryAllocations.resize(baseAllocation + NRI.CalculateAllocationNumber(*m_Device, resourceGroupDesc), nullptr);
    NRI_ABORT_ON_FAILURE( NRI.AllocateAndBindMemory(*m_Device, resourceGroupDesc, m_MemoryAllocations.data() + baseAllocation));

    // Create descriptors
    nri::Descriptor* descriptor = nullptr;

    {
        const nri::DeviceDesc& deviceDesc = NRI.GetDeviceDesc(*m_Device);

        nri::BufferViewDesc constantBufferViewDesc = {};
        constantBufferViewDesc.viewType = nri::BufferViewType::CONSTANT;
        constantBufferViewDesc.buffer = NRI.GetStreamerConstantBuffer(*m_Streamer);

        constantBufferViewDesc.size = helper::Align(sizeof(GlobalConstants), deviceDesc.constantBufferOffsetAlignment);
        NRI_ABORT_ON_FAILURE(NRI.CreateBufferView(constantBufferViewDesc, descriptor));
        m_Descriptors.push_back(descriptor);

        constantBufferViewDesc.size = helper::Align(sizeof(MorphMeshUpdateVerticesConstants), deviceDesc.constantBufferOffsetAlignment);
        NRI_ABORT_ON_FAILURE(NRI.CreateBufferView(constantBufferViewDesc, descriptor));
        m_Descriptors.push_back(descriptor);

        constantBufferViewDesc.size = helper::Align(sizeof(MorphMeshUpdatePrimitivesConstants), deviceDesc.constantBufferOffsetAlignment);
        NRI_ABORT_ON_FAILURE(NRI.CreateBufferView(constantBufferViewDesc, descriptor));
        m_Descriptors.push_back(descriptor);
    }

    for (const DescriptorDesc& desc : descriptorDescs)
    {
        if (desc.textureUsage == nri::TextureUsageBits::NONE)
        {
            if (desc.bufferUsage == nri::BufferUsageBits::CONSTANT_BUFFER)
            {
                // Constant buffer views are not stored in m_Descriptors
            }
            else
            {
                NRI.SetBufferDebugName(*(nri::Buffer*)desc.resource, desc.debugName);

                if (!(desc.bufferUsage & nri::BufferUsageBits::RAY_TRACING_BUFFER))
                {
                    if (desc.bufferUsage & nri::BufferUsageBits::SHADER_RESOURCE)
                    {
                        const nri::BufferViewDesc viewDesc = { (nri::Buffer*)desc.resource, nri::BufferViewType::SHADER_RESOURCE, desc.format };
                        NRI_ABORT_ON_FAILURE(NRI.CreateBufferView(viewDesc, descriptor));
                        m_Descriptors.push_back(descriptor);
                    }
                    if (desc.bufferUsage & nri::BufferUsageBits::SHADER_RESOURCE_STORAGE)
                    {
                        const nri::BufferViewDesc viewDesc = { (nri::Buffer*)desc.resource, nri::BufferViewType::SHADER_RESOURCE_STORAGE, desc.format };
                        NRI_ABORT_ON_FAILURE(NRI.CreateBufferView(viewDesc, descriptor));
                        m_Descriptors.push_back(descriptor);
                    }
                }
            }
        }
        else
        {
            NRI.SetTextureDebugName(*(nri::Texture*)desc.resource, desc.debugName);

            nri::Texture2DViewDesc viewDesc = {(nri::Texture*)desc.resource, desc.isArray ? nri::Texture2DViewType::SHADER_RESOURCE_2D_ARRAY : nri::Texture2DViewType::SHADER_RESOURCE_2D, desc.format};
            NRI_ABORT_ON_FAILURE(NRI.CreateTexture2DView(viewDesc, descriptor));
            m_Descriptors.push_back(descriptor);

            if (desc.textureUsage & nri::TextureUsageBits::SHADER_RESOURCE_STORAGE)
            {
                viewDesc.format = ConvertFormatToTextureStorageCompatible(desc.format);
                viewDesc.viewType = desc.isArray ? nri::Texture2DViewType::SHADER_RESOURCE_STORAGE_2D_ARRAY : nri::Texture2DViewType::SHADER_RESOURCE_STORAGE_2D;
                NRI_ABORT_ON_FAILURE(NRI.CreateTexture2DView(viewDesc, descriptor));
                m_Descriptors.push_back(descriptor);
            }
        }
    }
}

void Sample::CreateDescriptorSets()
{
    nri::DescriptorSet* descriptorSet = nullptr;


    { // Global constant buffer & samplers
        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 0, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::Descriptor* samplers[] =
        {
            Get(Descriptor::LinearMipmapLinear_Sampler),
            Get(Descriptor::LinearMipmapNearest_Sampler),
            Get(Descriptor::NearestMipmapNearest_Sampler),
        };

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { samplers, helper::GetCountOf(samplers) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);

        nri::Descriptor* constantBuffer = Get(Descriptor::Global_ConstantBuffer);
        NRI.UpdateDynamicConstantBuffers(*descriptorSet, 0, 1, &constantBuffer);
    }

    { // DescriptorSet::TraceAmbient1
        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Ambient_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 1, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::TraceOpaque1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::ComposedDiff_Texture),
            Get(Descriptor::ComposedSpec_ViewZ_Texture),
            Get(Descriptor::Ambient_Texture),
            Get(Descriptor((uint32_t)Descriptor::MaterialTextures + utils::StaticTexture::ScramblingRanking16spp)),
            Get(Descriptor((uint32_t)Descriptor::MaterialTextures + utils::StaticTexture::SobolSequence)),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Mv_StorageTexture),
            Get(Descriptor::ViewZ_StorageTexture),
            Get(Descriptor::Normal_Roughness_StorageTexture),
            Get(Descriptor::BaseColor_Metalness_StorageTexture),
            Get(Descriptor::DirectLighting_StorageTexture),
            Get(Descriptor::DirectEmission_StorageTexture),
            Get(Descriptor::PsrThroughput_StorageTexture),
            Get(Descriptor::Unfiltered_ShadowData_StorageTexture),
            Get(Descriptor::Unfiltered_Shadow_Translucency_StorageTexture),
            Get(Descriptor::Unfiltered_Diff_StorageTexture),
            Get(Descriptor::Unfiltered_Spec_StorageTexture),
#if( NRD_MODE == SH )
            Get(Descriptor::Unfiltered_DiffSh_StorageTexture),
            Get(Descriptor::Unfiltered_SpecSh_StorageTexture),
#endif
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Composition1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::ViewZ_Texture),
            Get(Descriptor::Normal_Roughness_Texture),
            Get(Descriptor::BaseColor_Metalness_Texture),
            Get(Descriptor::DirectLighting_Texture),
            Get(Descriptor::DirectEmission_Texture),
            Get(Descriptor::PsrThroughput_Texture),
            Get(Descriptor::Ambient_Texture),
            Get(Descriptor::Shadow_Texture),
            Get(Descriptor::Diff_Texture),
            Get(Descriptor::Spec_Texture),
#if( NRD_MODE == SH )
            Get(Descriptor::DiffSh_Texture),
            Get(Descriptor::SpecSh_Texture),
#endif
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::ComposedDiff_StorageTexture),
            Get(Descriptor::ComposedSpec_ViewZ_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::TraceTransparent1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::ViewZ_Texture),
            Get(Descriptor::ComposedDiff_Texture),
            Get(Descriptor::ComposedSpec_ViewZ_Texture),
            Get(Descriptor::Ambient_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Composed_ViewZ_StorageTexture),
            Get(Descriptor::Mv_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Temporal1a
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::Mv_Texture),
            Get(Descriptor::Composed_ViewZ_Texture),
            Get(Descriptor::TaaHistoryPrev_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::TaaHistory_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Temporal1b
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::Mv_Texture),
            Get(Descriptor::Composed_ViewZ_Texture),
            Get(Descriptor::TaaHistory_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::TaaHistoryPrev_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Upsample1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::DlssOutput_Texture),
            Get(Descriptor::Validation_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Upsample1a
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::TaaHistory_Texture),
            Get(Descriptor::Validation_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::Upsample1b
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::TaaHistoryPrev_Texture),
            Get(Descriptor::Validation_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::UpsampleNis1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::DlssOutput_Texture),
            Get(Descriptor::NisData1),
            Get(Descriptor::NisData2),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::UpsampleNis1a
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::TaaHistory_Texture),
            Get(Descriptor::NisData1),
            Get(Descriptor::NisData2),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::UpsampleNis1b
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::TaaHistoryPrev_Texture),
            Get(Descriptor::NisData1),
            Get(Descriptor::NisData2),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::Final_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::DlssBefore1
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::Composed_ViewZ_Texture),
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::ViewZ_StorageTexture),
            Get(Descriptor::Mv_StorageTexture),
            Get(Descriptor::DlssInput_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::DlssAfter1
        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::DlssOutput_StorageTexture),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 1, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { storageResources, helper::GetCountOf(storageResources) },
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 1, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::RayTracing2
        std::vector<nri::Descriptor*> textures(m_Scene.materials.size() * TEXTURES_PER_MATERIAL);
        for (size_t i = 0; i < m_Scene.materials.size(); i++)
        {
            const size_t index = i * TEXTURES_PER_MATERIAL;
            const utils::Material& material = m_Scene.materials[i];

            textures[index] = Get( Descriptor((uint32_t)Descriptor::MaterialTextures + material.baseColorTexIndex) );
            textures[index + 1] = Get( Descriptor((uint32_t)Descriptor::MaterialTextures + material.roughnessMetalnessTexIndex) );
            textures[index + 2] = Get( Descriptor((uint32_t)Descriptor::MaterialTextures + material.normalTexIndex) );
            textures[index + 3] = Get( Descriptor((uint32_t)Descriptor::MaterialTextures + material.emissiveTexIndex) );
        }

        const nri::Descriptor* structuredBuffers[] =
        {
            Get(Descriptor::InstanceData_Buffer),
            Get(Descriptor::PrimitiveData_Buffer),
            Get(Descriptor::MorphedPrimitivePrevData_Buffer),
        };

        const nri::Descriptor* accelerationStructures[] =
        {
            Get(Descriptor::World_AccelerationStructure),
            Get(Descriptor::Light_AccelerationStructure)
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 2, &descriptorSet, 1, helper::GetCountOf(textures)));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { accelerationStructures, helper::GetCountOf(accelerationStructures) },
            { structuredBuffers, helper::GetCountOf(structuredBuffers) },
            { textures.data(), helper::GetCountOf(textures) }
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);
    }

    { // DescriptorSet::MorphTargetPose3
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::MorphMeshVertices_Buffer)
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::MorphedPositions_StorageBuffer),
            Get(Descriptor::MorphedAttributes_StorageBuffer),
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 3, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) }
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);

        nri::Descriptor* constantBuffer = Get(Descriptor::MorphTargetPose_ConstantBuffer);
        NRI.UpdateDynamicConstantBuffers(*descriptorSet, 0, 1, &constantBuffer);
    }

    { // DescriptorSet::MorphTargetUpdatePrimitives3
        const nri::Descriptor* resources[] =
        {
            Get(Descriptor::MorphMeshIndices_Buffer),
            Get(Descriptor::MorphedPositions_Buffer),
            Get(Descriptor::MorphedAttributes_Buffer)
        };

        const nri::Descriptor* storageResources[] =
        {
            Get(Descriptor::PrimitiveData_StorageBuffer),
            Get(Descriptor::MorphedPrimitivePrevData_StorageBuffer)
        };

        NRI_ABORT_ON_FAILURE(NRI.AllocateDescriptorSets(*m_DescriptorPool, *m_PipelineLayout, 3, &descriptorSet, 1, 0));
        m_DescriptorSets.push_back(descriptorSet);

        const nri::DescriptorRangeUpdateDesc descriptorRangeUpdateDesc[] =
        {
            { resources, helper::GetCountOf(resources) },
            { storageResources, helper::GetCountOf(storageResources) }
        };

        NRI.UpdateDescriptorRanges(*descriptorSet, 0, helper::GetCountOf(descriptorRangeUpdateDesc), descriptorRangeUpdateDesc);

        nri::Descriptor* constantBuffer = Get(Descriptor::MorphTargetUpdatePrimitives_ConstantBuffer);
        NRI.UpdateDynamicConstantBuffers(*descriptorSet, 0, 1, &constantBuffer);
    }
}

void Sample::CreateTexture(std::vector<DescriptorDesc>& descriptorDescs, const char* debugName, nri::Format format, nri::Dim_t width, nri::Dim_t height, nri::Mip_t mipNum, nri::Dim_t arraySize, nri::TextureUsageBits usage, nri::AccessBits access)
{
    const nri::TextureDesc textureDesc = nri::Texture2D(format, width, height, mipNum, arraySize, usage);

    nri::Texture* texture = nullptr;
    NRI_ABORT_ON_FAILURE(NRI.CreateTexture(*m_Device, textureDesc, texture));
    m_Textures.push_back(texture);

    if (access != nri::AccessBits::UNKNOWN)
    {
        nri::Layout layout = nri::Layout::SHADER_RESOURCE;
        if (access & nri::AccessBits::COPY_SOURCE)
            layout = nri::Layout::COPY_SOURCE;
        else if (access & nri::AccessBits::COPY_DESTINATION)
            layout = nri::Layout::COPY_DESTINATION;
        else if (access & nri::AccessBits::SHADER_RESOURCE_STORAGE)
            layout = nri::Layout::SHADER_RESOURCE_STORAGE;

        nri::TextureBarrierDesc transition = nri::TextureBarrierFromUnknown(texture, {access, layout});
        m_TextureStates.push_back(transition);
        m_TextureFormats.push_back(format);
    }

    descriptorDescs.push_back( {debugName, texture, format, usage, nri::BufferUsageBits::NONE, arraySize > 1} );
}

void Sample::CreateBuffer(std::vector<DescriptorDesc>& descriptorDescs, const char* debugName, uint64_t elements, uint32_t stride, nri::BufferUsageBits usage, nri::Format format)
{
    if (!elements)
        elements = 1;

    nri::BufferDesc bufferDesc = {};
    bufferDesc.size = elements * stride;
    bufferDesc.structureStride = (format == nri::Format::UNKNOWN && stride != 1) ? stride : 0;
    bufferDesc.usageMask = usage;

    nri::Buffer* buffer = nullptr;
    NRI_ABORT_ON_FAILURE( NRI.CreateBuffer(*m_Device, bufferDesc, buffer) );
    m_Buffers.push_back(buffer);

    descriptorDescs.push_back( {debugName, buffer, format, nri::TextureUsageBits::NONE, usage} );
}

void Sample::UploadStaticData()
{
    std::vector<PrimitiveData> primitiveData( m_Scene.totalInstancedPrimitivesNum );

    for (utils::MeshInstance& meshInstance : m_Scene.meshInstances)
    {
        utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];
        uint32_t triangleNum = mesh.indexNum / 3;
        uint32_t staticPrimitiveOffset = mesh.indexOffset / 3;

        for (uint32_t j = 0; j < triangleNum; j++)
        {
            uint32_t staticPrimitiveIndex = staticPrimitiveOffset + j;

            const utils::UnpackedVertex& v0 = m_Scene.unpackedVertices[ mesh.vertexOffset + m_Scene.indices[staticPrimitiveIndex * 3] ];
            const utils::UnpackedVertex& v1 = m_Scene.unpackedVertices[ mesh.vertexOffset + m_Scene.indices[staticPrimitiveIndex * 3 + 1] ];
            const utils::UnpackedVertex& v2 = m_Scene.unpackedVertices[ mesh.vertexOffset + m_Scene.indices[staticPrimitiveIndex * 3 + 2] ];

            float2 n0 = Packed::EncodeUnitVector( float3(v0.normal), true );
            float2 n1 = Packed::EncodeUnitVector( float3(v1.normal), true );
            float2 n2 = Packed::EncodeUnitVector( float3(v2.normal), true );

            float2 t0 = Packed::EncodeUnitVector( float3(v0.tangent) + 1e-6f, true );
            float2 t1 = Packed::EncodeUnitVector( float3(v1.tangent) + 1e-6f, true );
            float2 t2 = Packed::EncodeUnitVector( float3(v2.tangent) + 1e-6f, true );

            PrimitiveData& data = primitiveData[meshInstance.primitiveOffset + j];
            data.uv0 = Packed::sf2_to_h2(v0.uv[0], v0.uv[1]);
            data.uv1 = Packed::sf2_to_h2(v1.uv[0], v1.uv[1]);
            data.uv2 = Packed::sf2_to_h2(v2.uv[0], v2.uv[1]);

            data.n0 = Packed::sf2_to_h2(n0.x, n0.y);
            data.n1 = Packed::sf2_to_h2(n1.x, n1.y);
            data.n2 = Packed::sf2_to_h2(n2.x, n2.y);

            data.t0 = Packed::sf2_to_h2(t0.x, t0.y);
            data.t1 = Packed::sf2_to_h2(t1.x, t1.y);
            data.t2 = Packed::sf2_to_h2(t2.x, t2.y);

            data.curvature0_curvature1 = Packed::sf2_to_h2(v0.curvature, v1.curvature);
            data.curvature2_bitangentSign = Packed::sf2_to_h2(v2.curvature, v0.tangent[3]);

            const utils::Primitive& primitive = m_Scene.primitives[staticPrimitiveIndex];
            data.worldToUvUnits = primitive.worldToUvUnits;
        }
    }

    // Gather subresources for read-only textures
    std::vector<nri::TextureSubresourceUploadDesc> subresources;
    subresources.push_back( {coef_scale_fp16, 1, (kFilterSize / 4) * 8, (kFilterSize / 4) * kPhaseCount * 8} );
    subresources.push_back( {coef_usm_fp16, 1, (kFilterSize / 4) * 8, (kFilterSize / 4) * kPhaseCount * 8} );
    for (const utils::Texture* texture : m_Scene.textures)
    {
        for (uint32_t layer = 0; layer < texture->GetArraySize(); layer++)
        {
            for (uint32_t mip = 0; mip < texture->GetMipNum(); mip++)
            {
                nri::TextureSubresourceUploadDesc subresource;
                texture->GetSubresource(subresource, mip, layer);

                subresources.push_back(subresource);
            }
        }
    }

    // Gather upload data for read-only textures
    std::vector<nri::TextureUploadDesc> textureUploadDescs;
    textureUploadDescs.push_back( {&subresources[0], Get(Texture::NisData1), {nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE}} );
    textureUploadDescs.push_back( {&subresources[1], Get(Texture::NisData2), {nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE}} );
    size_t subresourceOffset = 2;

    for (size_t i = 0; i < m_Scene.textures.size(); i++)
    {
        const utils::Texture* texture = m_Scene.textures[i];
        textureUploadDescs.push_back( {&subresources[subresourceOffset], Get( (Texture)((size_t)Texture::MaterialTextures + i) ), {nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE}} );

        nri::Mip_t mipNum = texture->GetMipNum();
        nri::Dim_t arraySize = texture->GetArraySize();
        subresourceOffset += size_t(arraySize) * size_t(mipNum);
    }

    // Append textures without data to initialize initial state
    for (const nri::TextureBarrierDesc& state : m_TextureStates)
    {
        nri::TextureUploadDesc desc = {};
        desc.after = {state.after.access, state.after.layout};
        desc.texture = (nri::Texture*)state.texture;

        textureUploadDescs.push_back(desc);
    }

    std::vector<utils::Index> morphMeshIndices(m_Scene.morphMeshTotalIndicesNum);
    uint32_t morphMeshIndexOffset = 0;

    // Compact static base pose data
    for (uint32_t morphMeshIndex : m_Scene.morphMeshes)
    {
        const utils::Mesh& mesh = m_Scene.meshes[morphMeshIndex];
        memcpy(morphMeshIndices.data() + morphMeshIndexOffset, &m_Scene.indices[mesh.indexOffset], mesh.indexNum * sizeof(m_Scene.indices[mesh.indexOffset]));
        morphMeshIndexOffset += mesh.indexNum;
    }

    // Buffer data
    nri::BufferUploadDesc bufferUploadDescs[] =
    {
        {primitiveData.data(), helper::GetByteSizeOf(primitiveData), Get(Buffer::PrimitiveData), 0, {nri::AccessBits::SHADER_RESOURCE}},
        {morphMeshIndices.data(), helper::GetByteSizeOf(morphMeshIndices), Get(Buffer::MorphMeshIndices), 0, {nri::AccessBits::SHADER_RESOURCE}},
        {m_Scene.morphVertices.data(), helper::GetByteSizeOf(m_Scene.morphVertices), Get(Buffer::MorphMeshVertices), 0, {nri::AccessBits::SHADER_RESOURCE}}
    };

    // Upload data and apply states
    NRI_ABORT_ON_FAILURE(NRI.UploadData(*m_CommandQueue, textureUploadDescs.data(), helper::GetCountOf(textureUploadDescs), bufferUploadDescs, helper::GetCountOf(bufferUploadDescs)));
}

void Sample::GatherInstanceData()
{
    bool isAnimatedObjects = m_Settings.animatedObjects;
    if (m_Settings.blink)
    {
        double period = 0.0003 * m_Timer.GetTimeStamp() * (m_Settings.animationSpeed < 0.0f ? 1.0f / (1.0f + Abs(m_Settings.animationSpeed)) : (1.0f + m_Settings.animationSpeed));
        isAnimatedObjects &= WaveTriangle(period) > 0.5;
    }

    uint64_t staticInstanceCount = m_Scene.instances.size() - m_AnimatedInstances.size();
    uint64_t instanceCount = staticInstanceCount + (isAnimatedObjects ? m_Settings.animatedObjectNum : 0);
    uint32_t instanceIndex = 0;

    m_InstanceData.clear();
    m_WorldTlasData.clear();
    m_LightTlasData.clear();

    float4x4 mCameraTranslation = float4x4::Identity();
    mCameraTranslation.AddTranslation( m_Camera.GetRelative(double3::Zero()) );
    mCameraTranslation.Transpose3x4();

    // Add static opaque (includes emissives)
    if (m_OpaqueObjectsNum)
    {
        nri::GeometryObjectInstance& tlasInstance = m_WorldTlasData.emplace_back();
        memcpy(tlasInstance.transform, mCameraTranslation.a16, sizeof(tlasInstance.transform));
        tlasInstance.instanceId = instanceIndex;
        tlasInstance.mask = FLAG_DEFAULT;
        tlasInstance.shaderBindingTableLocalOffset = 0;
        tlasInstance.flags = nri::TopLevelInstanceBits::TRIANGLE_CULL_DISABLE;
        tlasInstance.accelerationStructureHandle = NRI.GetAccelerationStructureHandle(*Get(AccelerationStructure::BLAS_StaticOpaque));

        instanceIndex += m_OpaqueObjectsNum;
    }

    // Add static transparent
    if (m_TransparentObjectsNum)
    {
        nri::GeometryObjectInstance& tlasInstance = m_WorldTlasData.emplace_back();
        memcpy(tlasInstance.transform, mCameraTranslation.a16, sizeof(tlasInstance.transform));
        tlasInstance.instanceId = instanceIndex;
        tlasInstance.mask = FLAG_TRANSPARENT;
        tlasInstance.shaderBindingTableLocalOffset = 0;
        tlasInstance.flags = nri::TopLevelInstanceBits::TRIANGLE_CULL_DISABLE;
        tlasInstance.accelerationStructureHandle = NRI.GetAccelerationStructureHandle(*Get(AccelerationStructure::BLAS_StaticTransparent));

        instanceIndex += m_TransparentObjectsNum;

        m_HasTransparent = m_TransparentObjectsNum ? true : false;
    }

    // Add static emissives (only emissives in a separate TLAS)
    if (m_EmissiveObjectsNum)
    {
        nri::GeometryObjectInstance& tlasInstance = m_LightTlasData.emplace_back();
        memcpy(tlasInstance.transform, mCameraTranslation.a16, sizeof(tlasInstance.transform));
        tlasInstance.instanceId = instanceIndex;
        tlasInstance.mask = FLAG_DEFAULT;
        tlasInstance.shaderBindingTableLocalOffset = 0;
        tlasInstance.flags = nri::TopLevelInstanceBits::TRIANGLE_CULL_DISABLE;
        tlasInstance.accelerationStructureHandle = NRI.GetAccelerationStructureHandle(*Get(AccelerationStructure::BLAS_StaticEmissive));

        instanceIndex += m_EmissiveObjectsNum;
    }

    // Gather instance data and add dynamic objects
    // IMPORTANT: instance data order must match geometry layout in BLAS-es
    for (uint32_t mode = (uint32_t)AccelerationStructure::BLAS_StaticOpaque; mode <= (uint32_t)AccelerationStructure::BLAS_Other; mode++)
    {
        for (size_t i = m_ProxyInstancesNum; i < instanceCount; i++)
        {
            utils::Instance& instance = m_Scene.instances[i];
            const utils::Material& material = m_Scene.materials[instance.materialIndex];

            if (material.IsOff())
                continue;

            if (mode == (uint32_t)AccelerationStructure::BLAS_StaticOpaque)
            {
                if (instance.allowUpdate || material.IsTransparent())
                    continue;
            }
            else if (mode == (uint32_t)AccelerationStructure::BLAS_StaticTransparent)
            {
                if (instance.allowUpdate || !material.IsTransparent())
                    continue;
            }
            else if (mode == (uint32_t)AccelerationStructure::BLAS_StaticEmissive)
            {
                if (instance.allowUpdate || !material.IsEmissive())
                    continue;
            }
            else
            {
                if (!instance.allowUpdate)
                    continue;
            }

            float4x4 mObjectToWorld = float4x4::Identity();
            float4x4 mOverloadedMatrix = float4x4::Identity();
            bool isLeftHanded = false;

            if (instance.allowUpdate)
            {
                const utils::MeshInstance& meshInstance = m_Scene.meshInstances[instance.meshInstanceIndex];
                const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

                // Current & previous transform
                mObjectToWorld = instance.rotation;
                float4x4 mObjectToWorldPrev = instance.rotationPrev;

                if (instance.scale != float3(1.0f))
                {
                    float4x4 translation;
                    translation.SetupByTranslation( ToFloat(instance.position) - mesh.aabb.GetCenter() );

                    float4x4 scale;
                    scale.SetupByScale(instance.scale);

                    float4x4 translationInv = translation;
                    translationInv.InvertOrtho();

                    float4x4 transform = translationInv * (scale * translation);

                    mObjectToWorld = mObjectToWorld * transform;
                    mObjectToWorldPrev = mObjectToWorldPrev * transform;
                }

                mObjectToWorld.AddTranslation( m_Camera.GetRelative(instance.position) );
                mObjectToWorldPrev.AddTranslation( m_Camera.GetRelative(instance.positionPrev) );

                if (mesh.HasMorphTargets())
                    mOverloadedMatrix = mObjectToWorldPrev;
                else
                {
                    // World to world (previous state) transform
                    // FP64 used to avoid imprecision problems on close up views (InvertOrtho can't be used due to scaling factors)
                    double4x4 dmWorldToObject = ToDouble(mObjectToWorld);
                    dmWorldToObject.Invert();

                    double4x4 dmObjectToWorldPrev = ToDouble(mObjectToWorldPrev);
                    mOverloadedMatrix = ToFloat(dmObjectToWorldPrev * dmWorldToObject);
                }

                // Update previous state
                instance.positionPrev = instance.position;
                instance.rotationPrev = instance.rotation;
            }
            else
            {
                mObjectToWorld = mCameraTranslation;

                // Static geometry doesn't have "prev" transformation, reuse this matrix to pass object rotation needed for normals
                mOverloadedMatrix = instance.rotation;

                // Transform can be left-handed (mirroring), in this case normals need flipping
                isLeftHanded = instance.rotation.IsLeftHanded();
            }

            mObjectToWorld.Transpose3x4();
            mOverloadedMatrix.Transpose3x4();

            // Add instance data
            const utils::MeshInstance& meshInstance = m_Scene.meshInstances[instance.meshInstanceIndex];
            uint32_t baseTextureIndex = instance.materialIndex * TEXTURES_PER_MATERIAL;
            float3 scale = instance.rotation.GetScale();

            uint32_t flags = FLAG_DEFAULT;

            if (!instance.allowUpdate)
                flags |= FLAG_STATIC;

            if (material.IsTransparent())
            {
                flags |= FLAG_TRANSPARENT;
                m_HasTransparent = true;
            }
            else if (m_Settings.emission && m_Settings.emissiveObjects && i > staticInstanceCount && (i % 3 == 0))
                flags |= FLAG_FORCED_EMISSION;

            if (meshInstance.morphedVertexOffset != utils::InvalidIndex)
                flags |= FLAG_DEFORMABLE;

            if (material.IsHair())
                flags |= FLAG_HAIR;

            InstanceData& instanceData = m_InstanceData.emplace_back();
            instanceData.mOverloadedMatrix0 = mOverloadedMatrix.col0;
            instanceData.mOverloadedMatrix1 = mOverloadedMatrix.col1;
            instanceData.mOverloadedMatrix2 = mOverloadedMatrix.col2;
            instanceData.baseColorAndMetalnessScale = material.baseColorAndMetalnessScale;
            instanceData.emissionAndRoughnessScale = material.emissiveAndRoughnessScale;
            instanceData.textureOffsetAndFlags = baseTextureIndex | ( flags << FLAG_FIRST_BIT );
            instanceData.primitiveOffset = meshInstance.primitiveOffset;
            instanceData.morphedPrimitiveOffset = meshInstance.morphedPrimitiveOffset;
            instanceData.invScale = (isLeftHanded ? -1.0f : 1.0f) / Max(scale.x, Max(scale.y, scale.z));

            // Add dynamic geometry
            if (instance.allowUpdate)
            {
                nri::GeometryObjectInstance tlasInstance = {};
                memcpy(tlasInstance.transform, mObjectToWorld.a16, sizeof(tlasInstance.transform));
                tlasInstance.instanceId = instanceIndex++;
                tlasInstance.mask = flags;
                tlasInstance.shaderBindingTableLocalOffset = 0;
                tlasInstance.flags = nri::TopLevelInstanceBits::TRIANGLE_CULL_DISABLE | (material.IsAlphaOpaque() ? nri::TopLevelInstanceBits::NONE : nri::TopLevelInstanceBits::FORCE_OPAQUE);
                tlasInstance.accelerationStructureHandle = NRI.GetAccelerationStructureHandle(*m_AccelerationStructures[meshInstance.blasIndex]);

                m_WorldTlasData.push_back(tlasInstance);

                if (flags == FLAG_FORCED_EMISSION || material.IsEmissive())
                    m_LightTlasData.push_back(tlasInstance);
            }
        }
    }

    {
        nri::BufferUpdateRequestDesc bufferUpdateRequestDesc = {};
        bufferUpdateRequestDesc.data = m_InstanceData.data();
        bufferUpdateRequestDesc.dataSize = m_InstanceData.size() * sizeof(InstanceData);
        bufferUpdateRequestDesc.dstBuffer = Get(Buffer::InstanceData);

        NRI.AddStreamerBufferUpdateRequest(*m_Streamer, bufferUpdateRequestDesc);
    }

    {
        nri::BufferUpdateRequestDesc bufferUpdateRequestDesc = {};
        bufferUpdateRequestDesc.data = m_WorldTlasData.data();
        bufferUpdateRequestDesc.dataSize = m_WorldTlasData.size() * sizeof(nri::GeometryObjectInstance);

        m_WorldTlasDataOffsetInDynamicBuffer = NRI.AddStreamerBufferUpdateRequest(*m_Streamer, bufferUpdateRequestDesc);
    }

    {
        nri::BufferUpdateRequestDesc bufferUpdateRequestDesc = {};
        bufferUpdateRequestDesc.data = m_LightTlasData.data();
        bufferUpdateRequestDesc.dataSize = m_LightTlasData.size() * sizeof(nri::GeometryObjectInstance);

        m_LightTlasDataOffsetInDynamicBuffer = NRI.AddStreamerBufferUpdateRequest(*m_Streamer, bufferUpdateRequestDesc);
    }
}

void Sample::UpdateConstantBuffer(uint32_t frameIndex, uint32_t maxAccumulatedFrameNum)
{
    const float3& sunDirection = GetSunDirection();

    uint32_t rectW = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
    uint32_t rectH = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);
    uint32_t rectWprev = uint32_t(m_RenderResolution.x * m_SettingsPrev.resolutionScale + 0.5f);
    uint32_t rectHprev = uint32_t(m_RenderResolution.y * m_SettingsPrev.resolutionScale + 0.5f);

    float2 renderSize = float2(float(m_RenderResolution.x), float(m_RenderResolution.y));
    float2 outputSize = float2(float(GetOutputResolution().x), float(GetOutputResolution().y));
    float2 windowSize = float2(float(GetWindowResolution().x), float(GetWindowResolution().y));
    float2 rectSize = float2( float(rectW), float(rectH) );
    float2 rectSizePrev = float2( float(rectWprev), float(rectHprev) );
    float2 jitter = (m_Settings.cameraJitter ? m_Camera.state.viewportJitter : 0.0f) / rectSize;

    float3 viewDir = float3(m_Camera.state.mViewToWorld.GetCol2().xmm) * (m_PositiveZ ? -1.0f : 1.0f);
    float3 cameraOrigin = m_Camera.state.position + m_Camera.state.mViewToWorld.GetCol3().xmm;

    float emissionIntensity = m_Settings.emissionIntensity * float(m_Settings.emission);
    float baseMipBias = ((m_Settings.TAA || IsDlssEnabled()) ? -0.5f : 0.0f) + log2f(m_Settings.resolutionScale);
    float nearZ = (m_PositiveZ ? 1.0f : -1.0f) * NEAR_Z * m_Settings.meterToUnitsMultiplier;
    float mipBias = baseMipBias + log2f(renderSize.x / outputSize.x);
    float taaMaxAccumulatedFramesNum = Min(0.25f * 1000.0f / m_Timer.GetSmoothedFrameTime(), 30.0f);
    float taaAccumulationFactor = taaMaxAccumulatedFramesNum / ( 1.0f + taaMaxAccumulatedFramesNum );

    nrd::HitDistanceParameters hitDistanceParameters = {};
    hitDistanceParameters.A = m_Settings.hitDistScale * m_Settings.meterToUnitsMultiplier;

    float minProbability = 0.0f;
    if (m_Settings.tracingMode == RESOLUTION_FULL_PROBABILISTIC)
    {
        nrd::HitDistanceReconstructionMode mode = nrd::HitDistanceReconstructionMode::OFF;
        if (m_Settings.denoiser == DENOISER_REBLUR)
            mode = m_ReblurSettings.hitDistanceReconstructionMode;
        else if (m_Settings.denoiser == DENOISER_RELAX)
            mode = m_RelaxSettings.hitDistanceReconstructionMode;

        // Min / max allowed probability to guarantee a sample in 3x3 or 5x5 area - https://godbolt.org/z/YGYo1rjnM
        if (mode == nrd::HitDistanceReconstructionMode::AREA_3X3)
            minProbability = 1.0f / 4.0f;
        else if (mode == nrd::HitDistanceReconstructionMode::AREA_5X5)
            minProbability = 1.0f / 16.0f;
    }

    float project[3];
    float4 frustum;
    uint32_t flags = 0;
    DecomposeProjection(STYLE_D3D, STYLE_D3D, m_Camera.state.mViewToClip, &flags, nullptr, nullptr, frustum.pv, project, nullptr);
    float orthoMode = ( flags & PROJ_ORTHO ) == 0 ? 0.0f : -1.0f;

    nri::DisplayDesc displayDesc = {};
    NRI.GetDisplayDesc(*m_SwapChain, displayDesc);

    m_SdrScale = displayDesc.sdrLuminance / 80.0f;

    // NIS
    NISConfig config = {};
    {
        float sharpness = m_Settings.sharpness + Lerp( (1.0f - m_Settings.sharpness) * 0.25f, 0.0f, (m_Settings.resolutionScale - 0.5f) * 2.0f );

        uint4 dimsOut = uint4(GetWindowResolution().x, GetWindowResolution().y, GetWindowResolution().x, GetWindowResolution().y);
        uint4 dimsIn = uint4(rectW, rectH, m_RenderResolution.x, m_RenderResolution.y);
        if (IsDlssEnabled())
            dimsIn = dimsOut;

        NVScalerUpdateConfig
        (
            config, sharpness,
            0, 0, dimsIn.x, dimsIn.y, dimsIn.z, dimsIn.w,
            0, 0, dimsOut.x, dimsOut.y, dimsOut.z, dimsOut.w,
            NISHDRMode::None
        );
    }

    GlobalConstants constants;
    {
        constants.gViewToWorld                                  = m_Camera.state.mViewToWorld;
        constants.gViewToClip                                   = m_Camera.state.mViewToClip;
        constants.gWorldToView                                  = m_Camera.state.mWorldToView;
        constants.gWorldToViewPrev                              = m_Camera.statePrev.mWorldToView;
        constants.gWorldToClip                                  = m_Camera.state.mWorldToClip;
        constants.gWorldToClipPrev                              = m_Camera.statePrev.mWorldToClip;
        constants.gHitDistParams                                = float4(hitDistanceParameters.A, hitDistanceParameters.B, hitDistanceParameters.C, hitDistanceParameters.D);
        constants.gCameraFrustum                                = frustum;
        constants.gSunDirection_gExposure                       = float4(sunDirection.x, sunDirection.y, sunDirection.z, m_Settings.exposure);
        constants.gCameraOrigin_gMipBias                        = float4(cameraOrigin.x, cameraOrigin.y, cameraOrigin.z, mipBias);
        constants.gViewDirection_gOrthoMode                     = float4(viewDir.x, viewDir.y, viewDir.z, orthoMode);
        constants.gHairBaseColorOverride                        = m_HairBaseColorOverride;
        constants.gHairBetasOverride                            = m_HairBetasOverride;
        constants.gWindowSize                                   = windowSize;
        constants.gOutputSize                                   = outputSize;
        constants.gRenderSize                                   = renderSize;
        constants.gRectSize                                     = rectSize;
        constants.gInvWindowSize                                = float2(1.0f, 1.0f) / windowSize;
        constants.gInvOutputSize                                = float2(1.0f, 1.0f) / outputSize;
        constants.gInvRenderSize                                = float2(1.0f, 1.0f) / renderSize;
        constants.gInvRectSize                                  = float2(1.0f, 1.0f) / rectSize;
        constants.gRectSizePrev                                 = rectSizePrev;
        constants.gNearZ                                        = nearZ;
        constants.gEmissionIntensity                            = emissionIntensity;
        constants.gJitter                                       = jitter;
        constants.gSeparator                                    = m_Settings.separator;
        constants.gRoughnessOverride                            = m_Settings.roughnessOverride;
        constants.gMetalnessOverride                            = m_Settings.metalnessOverride;
        constants.gUnitToMetersMultiplier                       = 1.0f / m_Settings.meterToUnitsMultiplier;
        constants.gIndirectDiffuse                              = m_Settings.indirectDiffuse ? 1.0f : 0.0f;
        constants.gIndirectSpecular                             = m_Settings.indirectSpecular ? 1.0f : 0.0f;
        constants.gTanSunAngularRadius                          = Tan( DegToRad( m_Settings.sunAngularDiameter * 0.5f ) );
        constants.gTanPixelAngularRadius                        = Tan( 0.5f * DegToRad(m_Settings.camFov) / outputSize.x );
        constants.gDebug                                        = m_Settings.debug;
        constants.gTransparent                                  = (m_HasTransparent && NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION && ( m_Settings.onScreen == 0 || m_Settings.onScreen == 2 )) ? 1.0f : 0.0f;
        constants.gPrevFrameConfidence                          = m_Settings.usePrevFrame ? 1.0f : 0.0f; // TODO: improve?
        constants.gMinProbability                               = minProbability;
        constants.gUnproject                                    = 1.0f / (0.5f * rectH * project[1]);
        constants.gAperture                                     = m_DofAperture * 0.01f;
        constants.gFocalDistance                                = m_DofFocalDistance;
        constants.gFocalLength                                  = ( 0.5f * ( 35.0f * 0.001f ) ) / Tan( DegToRad( m_Settings.camFov * 0.5f ) ); // for 35 mm sensor size (aka old-school 35 mm film)
        constants.gTAA                                          = (m_Settings.denoiser != DENOISER_REFERENCE && m_Settings.TAA) ? taaAccumulationFactor : 0;
        constants.gHdrScale                                     = displayDesc.isHDR ? 0.9f * displayDesc.maxLuminance / 80.0f : 1.0f;
        constants.gDenoiserType                                 = (uint32_t)m_Settings.denoiser;
        constants.gDisableShadowsAndEnableImportanceSampling    = (sunDirection.z < 0.0f && m_Settings.importanceSampling) ? 1 : 0;
        constants.gOnScreen                                     = m_Settings.onScreen + ((NRD_MODE == OCCLUSION || NRD_MODE == DIRECTIONAL_OCCLUSION) ? 3 : 0); // preserve original mapping
        constants.gFrameIndex                                   = frameIndex;
        constants.gForcedMaterial                               = m_Settings.forcedMaterial;
        constants.gUseNormalMap                                 = m_Settings.normalMap ? 1 : 0;
        constants.gIsWorldSpaceMotionEnabled                    = m_Settings.mvType == MV_3D ? 1 : 0;
        constants.gTracingMode                                  = m_Settings.tracingMode;
        constants.gSampleNum                                    = m_Settings.rpp;
        constants.gBounceNum                                    = m_Settings.bounceNum;
        constants.gResolve                                      = m_Settings.denoiser == DENOISER_REFERENCE ? false : m_Resolve;
        constants.gPSR                                          = m_Settings.PSR && m_Settings.tracingMode != RESOLUTION_HALF;
        constants.gValidation                                   = m_ShowValidationOverlay && m_Settings.denoiser != DENOISER_REFERENCE && m_Settings.separator != 1.0f;
        constants.gTrimLobe                                     = m_Settings.specularLobeTrimming ? 1 : 0;
        constants.gSR                                           = m_Settings.SR ? 1 : 0;
        constants.gRR                                           = m_Settings.RR ? 1 : 0;
        constants.gIsSrgb                                       = m_IsSrgb ? 1 : 0;
        constants.gAmbientMaxAccumulatedFramesNum               = m_ForceHistoryReset ? 0 : float(maxAccumulatedFrameNum);
        constants.gAmbient                                      = m_Settings.ambient;
        constants.gNisDetectRatio                               = config.kDetectRatio;
        constants.gNisDetectThres                               = config.kDetectThres;
        constants.gNisMinContrastRatio                          = config.kMinContrastRatio;
        constants.gNisRatioNorm                                 = config.kRatioNorm;
        constants.gNisContrastBoost                             = config.kContrastBoost;
        constants.gNisEps                                       = config.kEps;
        constants.gNisSharpStartY                               = config.kSharpStartY;
        constants.gNisSharpScaleY                               = config.kSharpScaleY;
        constants.gNisSharpStrengthMin                          = config.kSharpStrengthMin;
        constants.gNisSharpStrengthScale                        = config.kSharpStrengthScale;
        constants.gNisSharpLimitMin                             = config.kSharpLimitMin;
        constants.gNisSharpLimitScale                           = config.kSharpLimitScale;
        constants.gNisScaleX                                    = config.kScaleX;
        constants.gNisScaleY                                    = config.kScaleY;
        constants.gNisDstNormX                                  = config.kDstNormX;
        constants.gNisDstNormY                                  = config.kDstNormY;
        constants.gNisSrcNormX                                  = config.kSrcNormX;
        constants.gNisSrcNormY                                  = config.kSrcNormY;
        constants.gNisInputViewportOriginX                      = config.kInputViewportOriginX;
        constants.gNisInputViewportOriginY                      = config.kInputViewportOriginY;
        constants.gNisInputViewportWidth                        = config.kInputViewportWidth;
        constants.gNisInputViewportHeight                       = config.kInputViewportHeight;
        constants.gNisOutputViewportOriginX                     = config.kOutputViewportOriginX;
        constants.gNisOutputViewportOriginY                     = config.kOutputViewportOriginY;
        constants.gNisOutputViewportWidth                       = config.kOutputViewportWidth;
        constants.gNisOutputViewportHeight                      = config.kOutputViewportHeight;
    }

    m_GlobalConstantBufferOffset = NRI.UpdateStreamerConstantBuffer(*m_Streamer, &constants, sizeof(constants));
}

uint16_t Sample::BuildOptimizedTransitions(const TextureState* states, uint32_t stateNum, std::array<nri::TextureBarrierDesc, MAX_TEXTURE_TRANSITIONS_NUM>& transitions)
{
    uint32_t n = 0;

    for (uint32_t i = 0; i < stateNum; i++)
    {
        const TextureState& state = states[i];
        nri::TextureBarrierDesc& transition = GetState(state.texture);

        bool isStateChanged = transition.after.access != state.after.access || transition.after.layout != state.after.layout;
        bool isStorageBarrier = transition.after.access == nri::AccessBits::SHADER_RESOURCE_STORAGE && state.after.access == nri::AccessBits::SHADER_RESOURCE_STORAGE;
        if (isStateChanged || isStorageBarrier)
            transitions[n++] = nri::TextureBarrierFromState(transition, {state.after.access, state.after.layout});
    }

    return (uint16_t)n;
}

void Sample::RestoreBindings(nri::CommandBuffer& commandBuffer)
{
    NRI.CmdSetDescriptorPool(commandBuffer, *m_DescriptorPool);
    NRI.CmdSetPipelineLayout(commandBuffer, *m_PipelineLayout);
    NRI.CmdSetDescriptorSet(commandBuffer, 0, *Get(DescriptorSet::Global0), &m_GlobalConstantBufferOffset);
    NRI.CmdSetDescriptorSet(commandBuffer, 2, *Get(DescriptorSet::RayTracing2), nullptr);
}

void Sample::RenderFrame(uint32_t frameIndex)
{
    std::array<nri::TextureBarrierDesc, MAX_TEXTURE_TRANSITIONS_NUM> optimizedTransitions = {};

    const uint32_t bufferedFrameIndex = frameIndex % BUFFERED_FRAME_MAX_NUM;
    const Frame& frame = m_Frames[bufferedFrameIndex];
    nri::CommandBuffer& commandBuffer = *frame.commandBuffer;

    // Sizes
    uint32_t rectW = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
    uint32_t rectH = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);
    uint32_t rectGridW = (rectW + 15) / 16;
    uint32_t rectGridH = (rectH + 15) / 16;
    uint32_t outputGridW = (GetOutputResolution().x + 15) / 16;
    uint32_t outputGridH = (GetOutputResolution().y + 15) / 16;
    uint32_t windowGridW = (GetWindowResolution().x + 15) / 16;
    uint32_t windowGridH = (GetWindowResolution().y + 15) / 16;

    // NRD user pool
    NrdUserPool userPool = {};
    {
        // Common
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_MV, {&GetState(Texture::Mv), GetFormat(Texture::Mv)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_NORMAL_ROUGHNESS, {&GetState(Texture::Normal_Roughness), GetFormat(Texture::Normal_Roughness)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_VIEWZ, {&GetState(Texture::ViewZ), GetFormat(Texture::ViewZ)});

        // (Optional) Needed to allow IN_MV modification on the NRD side
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_BASECOLOR_METALNESS, {&GetState(Texture::BaseColor_Metalness), GetFormat(Texture::BaseColor_Metalness)});

        // (Optional) Validation
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_VALIDATION, {&GetState(Texture::Validation), GetFormat(Texture::Validation)});

        // Diffuse
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_DIFF_RADIANCE_HITDIST, {&GetState(Texture::Unfiltered_Diff), GetFormat(Texture::Unfiltered_Diff)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_DIFF_RADIANCE_HITDIST, {&GetState(Texture::Diff), GetFormat(Texture::Diff)});

        // Diffuse occlusion
    #if( NRD_MODE == OCCLUSION )
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_DIFF_HITDIST, {&GetState(Texture::Unfiltered_Diff), GetFormat(Texture::Unfiltered_Diff)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_DIFF_HITDIST, {&GetState(Texture::Diff), GetFormat(Texture::Diff)});
    #endif

        // Diffuse SH
    #if( NRD_MODE == SH )
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_DIFF_SH0, {&GetState(Texture::Unfiltered_Diff), GetFormat(Texture::Unfiltered_Diff)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_DIFF_SH1, {&GetState(Texture::Unfiltered_DiffSh), GetFormat(Texture::Unfiltered_DiffSh)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_DIFF_SH0, {&GetState(Texture::Diff), GetFormat(Texture::Diff)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_DIFF_SH1, {&GetState(Texture::DiffSh), GetFormat(Texture::DiffSh)});
    #endif

        // Diffuse directional occlusion
    #if( NRD_MODE == DIRECTIONAL_OCCLUSION )
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_DIFF_DIRECTION_HITDIST, {&GetState(Texture::Unfiltered_Diff), GetFormat(Texture::Unfiltered_Diff)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_DIFF_DIRECTION_HITDIST, {&GetState(Texture::Diff), GetFormat(Texture::Diff)});
    #endif

        // Specular
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SPEC_RADIANCE_HITDIST, {&GetState(Texture::Unfiltered_Spec), GetFormat(Texture::Unfiltered_Spec)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SPEC_RADIANCE_HITDIST, {&GetState(Texture::Spec), GetFormat(Texture::Spec)});

        // Specular occlusion
    #if( NRD_MODE == OCCLUSION )
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SPEC_HITDIST, {&GetState(Texture::Unfiltered_Spec), GetFormat(Texture::Unfiltered_Spec)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SPEC_HITDIST, {&GetState(Texture::Spec), GetFormat(Texture::Spec)});
    #endif

        // Specular SH
    #if( NRD_MODE == SH )
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SPEC_SH0, {&GetState(Texture::Unfiltered_Spec), GetFormat(Texture::Unfiltered_Spec)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SPEC_SH1, {&GetState(Texture::Unfiltered_SpecSh), GetFormat(Texture::Unfiltered_SpecSh)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SPEC_SH0, {&GetState(Texture::Spec), GetFormat(Texture::Spec)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SPEC_SH1, {&GetState(Texture::SpecSh), GetFormat(Texture::SpecSh)});
    #endif

        // SIGMA
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SHADOWDATA, {&GetState(Texture::Unfiltered_ShadowData), GetFormat(Texture::Unfiltered_ShadowData)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SHADOW_TRANSLUCENCY, {&GetState(Texture::Unfiltered_Shadow_Translucency), GetFormat(Texture::Unfiltered_Shadow_Translucency)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SHADOW_TRANSLUCENCY, {&GetState(Texture::Shadow), GetFormat(Texture::Shadow)});

        // REFERENCE
        NrdIntegration_SetResource(userPool, nrd::ResourceType::IN_SIGNAL, {&GetState(Texture::Composed_ViewZ), GetFormat(Texture::Composed_ViewZ)});
        NrdIntegration_SetResource(userPool, nrd::ResourceType::OUT_SIGNAL, {&GetState(Texture::Composed_ViewZ), GetFormat(Texture::Composed_ViewZ)});
    }

    const uint32_t dummyDynamicConstantOffset = 0;

    NRI.BeginCommandBuffer(commandBuffer, m_DescriptorPool);
    {
        // All-in-one pipeline layout
        NRI.CmdSetPipelineLayout(commandBuffer, *m_PipelineLayout);

        NRI.CmdSetDescriptorSet(commandBuffer, 0, *Get(DescriptorSet::Global0), &m_GlobalConstantBufferOffset);

        // Update morph animation
        if (m_Settings.activeAnimation < m_Scene.animations.size() && m_Scene.animations[m_Settings.activeAnimation].morphMeshInstances.size() && (!m_Settings.pauseAnimation || !m_SettingsPrev.pauseAnimation || frameIndex == 0))
        {
            const utils::Animation& animation = m_Scene.animations[m_Settings.activeAnimation];
            uint32_t animCurrBufferIndex = frameIndex & 0x1;
            uint32_t animPrevBufferIndex = frameIndex == 0 ? animCurrBufferIndex : 1 - animCurrBufferIndex;

            { // Update vertices
                helper::Annotation annotation(NRI, commandBuffer, "Morph mesh: update vertices");

                {
                    const nri::BufferBarrierDesc bufferTransitions[] =
                    {
                        // Output
                        {Get(Buffer::MorphedPositions), {nri::AccessBits::SHADER_RESOURCE}, {nri::AccessBits::SHADER_RESOURCE_STORAGE}},
                        {Get(Buffer::MorphedAttributes), {nri::AccessBits::SHADER_RESOURCE}, {nri::AccessBits::SHADER_RESOURCE_STORAGE}},
                    };

                    nri::BarrierGroupDesc transitionBarriers = { nullptr, bufferTransitions, nullptr, 0, (uint16_t)helper::GetCountOf(bufferTransitions), 0};
                    NRI.CmdBarrier(commandBuffer, transitionBarriers);
                }

                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::MorphMeshUpdateVertices));

                for (const utils::WeightTrackMorphMeshIndex& weightTrackMeshInstance : animation.morphMeshInstances)
                {
                    const utils::WeightsAnimationTrack& weightsTrack = animation.weightTracks[weightTrackMeshInstance.weightTrackIndex];
                    const utils::MeshInstance& meshInstance = m_Scene.meshInstances[weightTrackMeshInstance.meshInstanceIndex];
                    const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

                    uint32_t numShaderMorphTargets = Min((uint32_t)(weightsTrack.activeValues.size()), MORPH_MAX_ACTIVE_TARGETS_NUM);
                    float totalWeight = 0.f;
                    for (uint32_t i = 0; i < numShaderMorphTargets; i++)
                        totalWeight += weightsTrack.activeValues[i].second;
                    float renormalizeScale = 1.0f / totalWeight;

                    MorphMeshUpdateVerticesConstants constants = {};
                    {
                        for (uint32_t i = 0; i < numShaderMorphTargets; i++)
                        {
                            uint32_t morphTargetIndex = weightsTrack.activeValues[i].first;
                            uint32_t morphTargetVertexOffset = mesh.morphTargetVertexOffset + morphTargetIndex * mesh.vertexNum;

                            constants.gIndices[i / MORPH_ELEMENTS_PER_ROW_NUM].pv[i % MORPH_ELEMENTS_PER_ROW_NUM] = morphTargetVertexOffset;
                            constants.gWeights[i / MORPH_ELEMENTS_PER_ROW_NUM].pv[i % MORPH_ELEMENTS_PER_ROW_NUM] = renormalizeScale * weightsTrack.activeValues[i].second;
                        }
                        constants.gNumWeights = numShaderMorphTargets;
                        constants.gNumVertices = mesh.vertexNum;
                        constants.gPositionCurrFrameOffset = m_Scene.morphedVerticesNum * animCurrBufferIndex + meshInstance.morphedVertexOffset;
                        constants.gAttributesOutputOffset = meshInstance.morphedVertexOffset;
                    }

                    uint32_t dynamicConstantBufferOffset = NRI.UpdateStreamerConstantBuffer(*m_Streamer, &constants, sizeof(constants));
                    NRI.CmdSetDescriptorSet(commandBuffer, 3, *Get(DescriptorSet::MorphTargetPose3), &dynamicConstantBufferOffset);

                    constexpr uint32_t kNumThreads = 256;
                    NRI.CmdDispatch(commandBuffer, {(mesh.vertexNum + kNumThreads - 1) / kNumThreads, 1, 1});
                }

                {
                    const nri::BufferBarrierDesc bufferTransitions[] =
                    {
                        // Input
                        {Get(Buffer::MorphedPositions), {nri::AccessBits::SHADER_RESOURCE_STORAGE}, {nri::AccessBits::SHADER_RESOURCE}},
                        {Get(Buffer::MorphedAttributes), {nri::AccessBits::SHADER_RESOURCE_STORAGE}, {nri::AccessBits::SHADER_RESOURCE}},

                        // Output
                        {Get(Buffer::PrimitiveData), {nri::AccessBits::SHADER_RESOURCE}, {nri::AccessBits::SHADER_RESOURCE_STORAGE}},
                        {Get(Buffer::MorphedPrimitivePrevData), {nri::AccessBits::SHADER_RESOURCE}, {nri::AccessBits::SHADER_RESOURCE_STORAGE}},
                    };

                    nri::BarrierGroupDesc transitionBarriers = { nullptr, bufferTransitions, nullptr, 0, (uint16_t)helper::GetCountOf(bufferTransitions), 0 };
                    NRI.CmdBarrier(commandBuffer, transitionBarriers);
                }
            }

            { // Update primitives
                helper::Annotation annotation(NRI, commandBuffer, "Morph mesh: update primitives");

                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::MorphMeshUpdatePrimitives));

                for (const utils::WeightTrackMorphMeshIndex& weightTrackMeshInstance : animation.morphMeshInstances)
                {
                    const utils::MeshInstance& meshInstance = m_Scene.meshInstances[weightTrackMeshInstance.meshInstanceIndex];
                    const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];
                    uint32_t numPrimitives = mesh.indexNum / 3;

                    MorphMeshUpdatePrimitivesConstants constants = {};
                    {
                        constants.gPositionFrameOffsets.x = m_Scene.morphedVerticesNum * animCurrBufferIndex + meshInstance.morphedVertexOffset;
                        constants.gPositionFrameOffsets.y = m_Scene.morphedVerticesNum * animPrevBufferIndex + meshInstance.morphedVertexOffset;
                        constants.gNumPrimitives = numPrimitives;
                        constants.gIndexOffset = mesh.morphMeshIndexOffset;
                        constants.gAttributesOffset = meshInstance.morphedVertexOffset;
                        constants.gPrimitiveOffset = meshInstance.primitiveOffset;
                        constants.gMorphedPrimitiveOffset = meshInstance.morphedPrimitiveOffset;
                    }

                    uint32_t dynamicConstantBufferOffset = NRI.UpdateStreamerConstantBuffer(*m_Streamer, &constants, sizeof(constants));
                    NRI.CmdSetDescriptorSet(commandBuffer, 3, *Get(DescriptorSet::MorphTargetUpdatePrimitives3), &dynamicConstantBufferOffset);

                    constexpr uint32_t kNumThreads = 256;
                    NRI.CmdDispatch(commandBuffer, {(numPrimitives + kNumThreads - 1) / kNumThreads, 1, 1});
                }
            }

            { // Update BLAS
                helper::Annotation annotation(NRI, commandBuffer, "Morph mesh: BLAS");

                size_t scratchOffset = 0;
                for (const utils::WeightTrackMorphMeshIndex& weightTrackMeshInstance : animation.morphMeshInstances)
                {
                    const utils::MeshInstance& meshInstance = m_Scene.meshInstances[weightTrackMeshInstance.meshInstanceIndex];
                    const utils::Mesh& mesh = m_Scene.meshes[meshInstance.meshIndex];

                    nri::GeometryObject geometryObject = {};
                    geometryObject.type = nri::GeometryType::TRIANGLES;
                    geometryObject.flags = nri::BottomLevelGeometryBits::NONE; // will be set in TLAS instance
                    geometryObject.triangles.vertexBuffer = Get(Buffer::MorphedPositions);
                    geometryObject.triangles.vertexStride = sizeof(float[4]); // underlying storage is RGBA32_SFLOAT for UAV
                    geometryObject.triangles.vertexOffset = geometryObject.triangles.vertexStride * (m_Scene.morphedVerticesNum * animCurrBufferIndex + meshInstance.morphedVertexOffset);
                    geometryObject.triangles.vertexNum = mesh.vertexNum;
                    geometryObject.triangles.vertexFormat = nri::Format::RGB32_SFLOAT;
                    geometryObject.triangles.indexBuffer = Get(Buffer::MorphMeshIndices);
                    geometryObject.triangles.indexOffset = mesh.morphMeshIndexOffset * sizeof(utils::Index);
                    geometryObject.triangles.indexNum = mesh.indexNum;
                    geometryObject.triangles.indexType = sizeof(utils::Index) == 2 ? nri::IndexType::UINT16 : nri::IndexType::UINT32;

                    nri::AccelerationStructure& accelerationStructure = *m_AccelerationStructures[meshInstance.blasIndex];
                    NRI.CmdBuildBottomLevelAccelerationStructure(commandBuffer, 1, &geometryObject, BLAS_DEFORMABLE_MESH_BUILD_BITS, accelerationStructure, *Get(Buffer::MorphMeshScratch), scratchOffset);

                    uint64_t size = NRI.GetAccelerationStructureBuildScratchBufferSize(accelerationStructure);
                    scratchOffset += helper::Align(size, 256);
                }

                {
                    const nri::BufferBarrierDesc bufferTransitions[] =
                    {
                        {Get(Buffer::PrimitiveData), {nri::AccessBits::SHADER_RESOURCE_STORAGE}, {nri::AccessBits::SHADER_RESOURCE}},
                        {Get(Buffer::MorphedPrimitivePrevData), {nri::AccessBits::SHADER_RESOURCE_STORAGE}, {nri::AccessBits::SHADER_RESOURCE}},
                    };

                    nri::BarrierGroupDesc transitionBarriers = { nullptr, bufferTransitions, nullptr, 0, (uint16_t)helper::GetCountOf(bufferTransitions), 0 };
                    NRI.CmdBarrier(commandBuffer, transitionBarriers);
                }
            }
        }

        { // Copy upload requests to destinations
            helper::Annotation annotation(NRI, commandBuffer, "Streamer");

            // TODO: is barrier from "SHADER_RESOURCE" to "COPY_DESTINATION" needed here for "Buffer::InstanceData"?

            NRI.CmdUploadStreamerUpdateRequests(commandBuffer, *m_Streamer);
        }

        { // TLAS
            helper::Annotation annotation(NRI, commandBuffer, "TLAS");

            nri::Buffer* dynamicBuffer = NRI.GetStreamerDynamicBuffer(*m_Streamer);
            NRI.CmdBuildTopLevelAccelerationStructure(commandBuffer, (uint32_t)m_WorldTlasData.size(), *dynamicBuffer, m_WorldTlasDataOffsetInDynamicBuffer, TLAS_BUILD_BITS, *Get(AccelerationStructure::TLAS_World), *Get(Buffer::WorldScratch), 0);
            NRI.CmdBuildTopLevelAccelerationStructure(commandBuffer, (uint32_t)m_LightTlasData.size(), *dynamicBuffer, m_LightTlasDataOffsetInDynamicBuffer, TLAS_BUILD_BITS, *Get(AccelerationStructure::TLAS_Emissive), *Get(Buffer::LightScratch), 0);

            {
                const nri::BufferBarrierDesc transition = {Get(Buffer::InstanceData), {nri::AccessBits::COPY_DESTINATION}, {nri::AccessBits::SHADER_RESOURCE}};

                nri::BarrierGroupDesc barrierGroupDesc = {};
                barrierGroupDesc.buffers = &transition;
                barrierGroupDesc.bufferNum = 1;

                NRI.CmdBarrier(commandBuffer, barrierGroupDesc);
            }
        }

        // Must be bound here, after updating "Buffer::InstanceData"
        NRI.CmdSetDescriptorSet(commandBuffer, 2, *Get(DescriptorSet::RayTracing2), nullptr);

        // Trace ambient // TODO: replace with a hash-grid based radiance cache
        if (m_Settings.ambient)
        {
            helper::Annotation annotation(NRI, commandBuffer, "Trace ambient");

            const TextureState transitions[] =
            {
                // Output
                {Texture::Ambient, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
            };

            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::TraceAmbient));
            NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::TraceAmbient1), &dummyDynamicConstantOffset);

            NRI.CmdDispatch(commandBuffer, {2, 2, 1});
        }

        { // Trace opaque
            helper::Annotation annotation(NRI, commandBuffer, "Trace opaque");

            const TextureState transitions[] =
            {
                // Input
                {Texture::ComposedDiff, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::ComposedSpec_ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Ambient, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                // Output
                {Texture::Mv, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::ViewZ, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Normal_Roughness, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::BaseColor_Metalness, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::DirectLighting, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::DirectEmission, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::PsrThroughput, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Unfiltered_ShadowData, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Unfiltered_Shadow_Translucency, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Unfiltered_Diff, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Unfiltered_Spec, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
#if( NRD_MODE == SH )
                {Texture::Unfiltered_DiffSh, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Unfiltered_SpecSh, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
#endif
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::TraceOpaque));
            NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::TraceOpaque1), &dummyDynamicConstantOffset);

            uint32_t rectWmod = uint32_t(m_RenderResolution.x * m_Settings.resolutionScale + 0.5f);
            uint32_t rectHmod = uint32_t(m_RenderResolution.y * m_Settings.resolutionScale + 0.5f);
            uint32_t rectGridWmod = (rectWmod + 15) / 16;
            uint32_t rectGridHmod = (rectHmod + 15) / 16;

            NRI.CmdDispatch(commandBuffer, {rectGridWmod, rectGridHmod, 1});
        }

    #if( NRD_MODE != OCCLUSION && NRD_MODE != DIRECTIONAL_OCCLUSION )
        { // Shadow denoising
            helper::Annotation annotation(NRI, commandBuffer, "Shadow denoising");

            float3 sunDir = GetSunDirection();

            nrd::SigmaSettings shadowSettings = {};
            shadowSettings.lightDirection[0] = sunDir.x;
            shadowSettings.lightDirection[1] = sunDir.y;
            shadowSettings.lightDirection[2] = sunDir.z;

            nrd::Identifier denoiser = NRD_ID(SIGMA_SHADOW_TRANSLUCENCY);

            m_NRD.SetDenoiserSettings(denoiser, &shadowSettings);
            m_NRD.Denoise(&denoiser, 1, commandBuffer, userPool);

            //RestoreBindings(commandBuffer, frame); // Bindings will be restored in the next section
        }
    #endif

        { // Opaque Denoising
            helper::Annotation annotation(NRI, commandBuffer, "Opaque denoising");

            if (m_Settings.denoiser == DENOISER_REBLUR || m_Settings.denoiser == DENOISER_REFERENCE)
            {
                nrd::HitDistanceParameters hitDistanceParameters = {};
                hitDistanceParameters.A = m_Settings.hitDistScale * m_Settings.meterToUnitsMultiplier;
                m_ReblurSettings.hitDistanceParameters = hitDistanceParameters;

                nrd::ReblurSettings settings = m_ReblurSettings;
            #if( NRD_MODE == SH || NRD_MODE == DIRECTIONAL_OCCLUSION )
                // High quality SG resolve allows to use more relaxed normal weights
                if (m_Resolve)
                    settings.lobeAngleFraction *= 1.333f;
            #endif

        #if( NRD_MODE == OCCLUSION )
            #if( NRD_COMBINED == 1 )
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_SPECULAR_OCCLUSION)};
            #else
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_OCCLUSION), NRD_ID(REBLUR_SPECULAR_OCCLUSION)};
            #endif
        #elif( NRD_MODE == SH )
            #if( NRD_COMBINED == 1 )
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_SPECULAR_SH)};
            #else
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_SH), NRD_ID(REBLUR_SPECULAR_SH)};
            #endif
        #elif( NRD_MODE == DIRECTIONAL_OCCLUSION )
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_DIRECTIONAL_OCCLUSION)};
        #else
            #if( NRD_COMBINED == 1 )
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE_SPECULAR)};
            #else
                const nrd::Identifier denoisers[] = {NRD_ID(REBLUR_DIFFUSE), NRD_ID(REBLUR_SPECULAR)};
            #endif
        #endif

                for (uint32_t i = 0; i < helper::GetCountOf(denoisers); i++)
                    m_NRD.SetDenoiserSettings(denoisers[i], &settings);

                m_NRD.Denoise(denoisers, helper::GetCountOf(denoisers), commandBuffer, userPool);
            }
            else if (m_Settings.denoiser == DENOISER_RELAX)
            {
                nrd::RelaxSettings settings = m_RelaxSettings;
            #if( NRD_MODE == SH || NRD_MODE == DIRECTIONAL_OCCLUSION )
                // High quality SG resolve allows to use more relaxed normal weights
                if (m_Resolve)
                {
                    settings.diffuseLobeAngleFraction *= 1.333f;
                    settings.specularLobeAngleFraction *= 1.333f;
                }
            #endif

                #if( NRD_COMBINED == 1 )
                    #if( NRD_MODE == SH )
                        const nrd::Identifier denoisers[] = {NRD_ID(RELAX_DIFFUSE_SPECULAR_SH)};
                    #else
                        const nrd::Identifier denoisers[] = {NRD_ID(RELAX_DIFFUSE_SPECULAR)};
                    #endif
                #else
                    #if( NRD_MODE == SH )
                        const nrd::Identifier denoisers[] = {NRD_ID(RELAX_DIFFUSE_SH), NRD_ID(RELAX_SPECULAR_SH)};
                    #else
                        const nrd::Identifier denoisers[] = {NRD_ID(RELAX_DIFFUSE), NRD_ID(RELAX_SPECULAR)};
                    #endif
                #endif

                for (uint32_t i = 0; i < helper::GetCountOf(denoisers); i++)
                    m_NRD.SetDenoiserSettings(denoisers[i], &settings);

                m_NRD.Denoise(denoisers, helper::GetCountOf(denoisers), commandBuffer, userPool);
            }

            RestoreBindings(commandBuffer);
        }

        { // Composition
            helper::Annotation annotation(NRI, commandBuffer, "Composition");

            const TextureState transitions[] =
            {
                // Input
                {Texture::ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Normal_Roughness, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::BaseColor_Metalness, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::DirectLighting, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::DirectEmission, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::PsrThroughput, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Ambient, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Shadow, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Diff, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Spec, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
            #if( NRD_MODE == SH )
                {Texture::DiffSh, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::SpecSh, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
            #endif
                // Output
                {Texture::ComposedDiff, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::ComposedSpec_ViewZ, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::Composition));
            NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::Composition1), &dummyDynamicConstantOffset);

            NRI.CmdDispatch(commandBuffer, {rectGridW, rectGridH, 1});
        }

        { // Trace transparent
            helper::Annotation annotation(NRI, commandBuffer, "Trace transparent");

            const TextureState transitions[] =
            {
                // Input
                {Texture::ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::ComposedDiff, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::ComposedSpec_ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                // Output
                {Texture::Composed_ViewZ, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                {Texture::Mv, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::TraceTransparent));
            NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::TraceTransparent1), &dummyDynamicConstantOffset);

            NRI.CmdDispatch(commandBuffer, {rectGridW, rectGridH, 1});
        }

        if (m_Settings.denoiser == DENOISER_REFERENCE)
        { // Reference
            helper::Annotation annotation(NRI, commandBuffer, "Reference denoising");

            m_CommonSettings.rectSize[0] = (uint16_t)m_RenderResolution.x;
            m_CommonSettings.rectSize[1] = (uint16_t)m_RenderResolution.y;
            m_CommonSettings.splitScreen = m_Settings.separator;

            nrd::Identifier denoiser = NRD_ID(REFERENCE);

            m_NRD.SetCommonSettings(m_CommonSettings);
            m_NRD.SetDenoiserSettings(denoiser, &m_ReferenceSettings);
            m_NRD.Denoise(&denoiser, 1, commandBuffer, userPool);

            RestoreBindings(commandBuffer);
        }

        const bool isEven = !(frameIndex & 0x1);
        const Texture taaSrc = isEven ? Texture::TaaHistoryPrev : Texture::TaaHistory;
        const Texture taaDst = isEven ? Texture::TaaHistory : Texture::TaaHistoryPrev;

        if (IsDlssEnabled())
        {
            { // Before DLSS
                helper::Annotation annotation(NRI, commandBuffer, "Before Dlss");

                const TextureState transitions[] =
                {
                    // Input
                    {Texture::Composed_ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                    // Output
                    {Texture::ViewZ, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                    {Texture::Mv, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                    {Texture::DlssInput, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                };
                nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
                NRI.CmdBarrier(commandBuffer, transitionBarriers);

                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::DlssBefore));
                NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::DlssBefore1), &dummyDynamicConstantOffset);

                NRI.CmdDispatch(commandBuffer, {rectGridW, rectGridH, 1});
            }

            { // DLSS
                helper::Annotation annotation(NRI, commandBuffer, "Dlss");

                const TextureState transitions[] =
                {
                    // Input
                    {Texture::ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                    {Texture::Mv, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                    {Texture::DlssInput, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                    // Output
                    {Texture::DlssOutput, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                };
                nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
                NRI.CmdBarrier(commandBuffer, transitionBarriers);

                DlssDispatchDesc dlssDesc = {};
                dlssDesc.texOutput = {Get(Texture::DlssOutput), Get(Descriptor::DlssOutput_StorageTexture), GetFormat(Texture::DlssOutput), {GetOutputResolution().x, GetOutputResolution().y}};
                dlssDesc.texInput = {Get(Texture::DlssInput), Get(Descriptor::DlssInput_Texture), GetFormat(Texture::DlssInput), {m_RenderResolution.x, m_RenderResolution.y}};
                dlssDesc.texMv = {Get(Texture::Mv), Get(Descriptor::Mv_Texture), GetFormat(Texture::Mv), {m_RenderResolution.x, m_RenderResolution.y}};
                dlssDesc.texDepth = {Get(Texture::ViewZ), Get(Descriptor::ViewZ_Texture), GetFormat(Texture::ViewZ), {m_RenderResolution.x, m_RenderResolution.y}};
                dlssDesc.viewportDims = {rectW, rectH};
                dlssDesc.mvScale[0] = 1.0f;
                dlssDesc.mvScale[1] = 1.0f;
                dlssDesc.jitter[0] = -m_Camera.state.viewportJitter.x;
                dlssDesc.jitter[1] = -m_Camera.state.viewportJitter.y;
                dlssDesc.reset = m_ForceHistoryReset || m_Settings.SR != m_SettingsPrev.SR;

                m_DLSS.Evaluate(&commandBuffer, dlssDesc);

                RestoreBindings(commandBuffer); // TODO: is it needed?
            }

            { // After DLSS
                helper::Annotation annotation(NRI, commandBuffer, "After Dlss");

                const TextureState transitions[] =
                {
                    // Output
                    {Texture::DlssOutput, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
                };
                nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
                NRI.CmdBarrier(commandBuffer, transitionBarriers);

                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::DlssAfter));
                NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::DlssAfter1), &dummyDynamicConstantOffset);

                NRI.CmdDispatch(commandBuffer, {outputGridW, outputGridH, 1});
            }
        }
        else
        {
            // Temporal
            helper::Annotation annotation(NRI, commandBuffer, "Temporal");

            const TextureState transitions[] =
            {
                // Input
                {Texture::Mv, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Composed_ViewZ, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {taaSrc, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                // Output
                {taaDst, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::Temporal));
            NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(isEven ? DescriptorSet::Temporal1a : DescriptorSet::Temporal1b), &dummyDynamicConstantOffset);

            NRI.CmdDispatch(commandBuffer, {rectGridW, rectGridH, 1});
        }

        { // Upsample, NRD validation and split screen
            helper::Annotation annotation(NRI, commandBuffer, "Upsample");

            const TextureState transitions[] =
            {
                // Input
                {IsDlssEnabled() ? Texture::DlssOutput : taaDst, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                {Texture::Validation, nri::AccessBits::SHADER_RESOURCE, nri::Layout::SHADER_RESOURCE},
                // Output
                {Texture::Final, nri::AccessBits::SHADER_RESOURCE_STORAGE, nri::Layout::SHADER_RESOURCE_STORAGE},
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, optimizedTransitions.data(), 0, 0, BuildOptimizedTransitions(transitions, helper::GetCountOf(transitions), optimizedTransitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            if (IsNisAllowed() && m_Settings.NIS)
            {
                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::UpsampleNis));
                if (IsDlssEnabled())
                    NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::UpsampleNis1), &dummyDynamicConstantOffset);
                else
                    NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(isEven ? DescriptorSet::UpsampleNis1a : DescriptorSet::UpsampleNis1b), &dummyDynamicConstantOffset);

                // See NIS_Config.h
                windowGridW = (GetWindowResolution().x + 31) / 32;
                windowGridH = (GetWindowResolution().y + 31) / 32;
            }
            else
            {
                NRI.CmdSetPipeline(commandBuffer, *Get(Pipeline::Upsample));
                if (IsDlssEnabled())
                    NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(DescriptorSet::Upsample1), &dummyDynamicConstantOffset);
                else
                    NRI.CmdSetDescriptorSet(commandBuffer, 1, *Get(isEven ? DescriptorSet::Upsample1a : DescriptorSet::Upsample1b), &dummyDynamicConstantOffset);
            }

            NRI.CmdDispatch(commandBuffer, {windowGridW, windowGridH, 1});
        }

        const uint32_t backBufferIndex = NRI.AcquireNextSwapChainTexture(*m_SwapChain);
        const BackBuffer* backBuffer = &m_SwapChainBuffers[backBufferIndex];

        { // Copy to back-buffer
            helper::Annotation annotation(NRI, commandBuffer, "Copy to back buffer");

            const nri::TextureBarrierDesc transitions[] =
            {
                nri::TextureBarrierFromState(GetState(Texture::Final), {nri::AccessBits::COPY_SOURCE, nri::Layout::COPY_SOURCE}),
                nri::TextureBarrierFromUnknown(backBuffer->texture, {nri::AccessBits::COPY_DESTINATION, nri::Layout::COPY_DESTINATION}),
            };
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, transitions, 0, 0, (uint16_t)helper::GetCountOf(transitions)};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            NRI.CmdCopyTexture(commandBuffer, *backBuffer->texture, nullptr, *Get(Texture::Final), nullptr);
        }

        { // UI
            nri::TextureBarrierDesc beforeTransitions = nri::TextureBarrier(backBuffer->texture, {nri::AccessBits::COPY_DESTINATION, nri::Layout::COPY_DESTINATION, nri::StageBits::COPY}, {nri::AccessBits::COLOR_ATTACHMENT, nri::Layout::COLOR_ATTACHMENT, nri::StageBits::COLOR_ATTACHMENT});
            nri::BarrierGroupDesc transitionBarriers = {nullptr, nullptr, &beforeTransitions, 0, 0, 1};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);

            nri::AttachmentsDesc desc = {};
            desc.colors = &backBuffer->colorAttachment;
            desc.colorNum = 1;

            NRI.CmdBeginRendering(commandBuffer, desc);
            RenderUI(NRI, NRI, *m_Streamer, commandBuffer, m_SdrScale, m_IsSrgb);
            NRI.CmdEndRendering(commandBuffer);

            const nri::TextureBarrierDesc afterTransitions = nri::TextureBarrierFromState(beforeTransitions, {nri::AccessBits::UNKNOWN, nri::Layout::PRESENT, nri::StageBits::ALL});
            transitionBarriers = {nullptr, nullptr, &afterTransitions, 0, 0, 1};
            NRI.CmdBarrier(commandBuffer, transitionBarriers);
        }
    }
    NRI.EndCommandBuffer(commandBuffer);

    { // Submit
        nri::FenceSubmitDesc signalFence = {};
        signalFence.fence = m_FrameFence;
        signalFence.value = 1 + frameIndex;

        nri::QueueSubmitDesc queueSubmitDesc = {};
        queueSubmitDesc.commandBuffers = &frame.commandBuffer;
        queueSubmitDesc.commandBufferNum = 1;
        queueSubmitDesc.signalFences = &signalFence;
        queueSubmitDesc.signalFenceNum = 1;

        NRI.QueueSubmit(*m_CommandQueue, queueSubmitDesc);
    }

    // Present
    NRI.QueuePresent(*m_SwapChain);

    // Cap FPS if requested
    float msLimit = m_Settings.limitFps ? 1000.0f / m_Settings.maxFps : 0.0f;
    double lastFrameTimeStamp = m_Timer.GetLastFrameTimeStamp();

    while (m_Timer.GetTimeStamp() - lastFrameTimeStamp < msLimit)
        ;
}

SAMPLE_MAIN(Sample, 0);
